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] Unit
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 //
24 // DEPRECATED in favour of `fs.read_to_string`, which declares
25 // [fs_read]. This op takes a PATH and is gated by
26 // --allow-fs-read, but declares [io] — documented as
27 // "console / stdio" — so an effect row carrying [io] tells a
28 // reviewer nothing about filesystem reach (#882). Kept working
29 // because ~160 call sites across the fleet use it; re-typing it
30 // in place would change every declared row at once, which is
31 // the failure mode #808 already produced.
32 fields.insert("read".into(), Ty::function(
33 vec![Ty::str()],
34 EffectSet::singleton("io"),
35 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
36 ));
37 // io.write(path :: Str, contents :: Str) -> [io] Result[Unit, Str]
38 // DEPRECATED in favour of `fs.write` ([fs_write]); see io.read.
39 fields.insert("write".into(), Ty::function(
40 vec![Ty::str(), Ty::str()],
41 EffectSet::singleton("io"),
42 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
43 ));
44 // io.readline() -> [io] Option[Str]
45 fields.insert("readline".into(), Ty::function(
46 vec![],
47 EffectSet::singleton("io"),
48 Ty::Con("Option".into(), vec![Ty::str()]),
49 ));
50 // io.argv() -> [io] List[Str]
51 fields.insert("argv".into(), Ty::function(
52 vec![],
53 EffectSet::singleton("io"),
54 Ty::List(Box::new(Ty::str())),
55 ));
56 Some(Ty::Record(fields))
57 }
58 // Declared in `crate::stdlib_spec` (#778): signatures, docs and
59 // the runtime table share one definition per builtin.
60 "str" | "list" => crate::stdlib_spec::module_record(name),
61 "int" => {
62 let mut fields = IndexMap::new();
63 fields.insert("to_str".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::str()));
64 fields.insert("to_float".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::float()));
65 // Int scalar helpers (#681). math.{min,max,abs} are Float-only;
66 // these are the integer equivalents so Int callers don't have to
67 // round-trip through Float (which is lossy for large ints).
68 fields.insert("abs".into(), Ty::function(vec![Ty::int()], EffectSet::empty(), Ty::int()));
69 for name in &["min", "max"] {
70 fields.insert((*name).into(), Ty::function(
71 vec![Ty::int(), Ty::int()], EffectSet::empty(), Ty::int()));
72 }
73 Some(Ty::Record(fields))
74 }
75 "math" => {
76 let mut fields = IndexMap::new();
77 // Matrix is registered as a built-in type alias in
78 // TypeEnv::new_with_builtins; refer to it nominally so call
79 // sites unify against the user's `:: Matrix` annotations.
80 let mat = || Ty::Con("Matrix".into(), Vec::new());
81 // Scalar floats — single-arg `Float -> Float`.
82 for name in &[
83 "exp", "log", "log2", "log10", "sqrt", "abs",
84 "sin", "cos", "tan", "asin", "acos", "atan",
85 "floor", "ceil", "round", "trunc",
86 ] {
87 fields.insert((*name).into(), Ty::function(
88 vec![Ty::float()], EffectSet::empty(), Ty::float(),
89 ));
90 }
91 // Two-arg `Float, Float -> Float`.
92 for name in &["pow", "atan2", "min", "max"] {
93 fields.insert((*name).into(), Ty::function(
94 vec![Ty::float(), Ty::float()], EffectSet::empty(), Ty::float(),
95 ));
96 }
97 // Constructors.
98 fields.insert("zeros".into(), Ty::function(
99 vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
100 ));
101 fields.insert("ones".into(), Ty::function(
102 vec![Ty::int(), Ty::int()], EffectSet::empty(), mat(),
103 ));
104 fields.insert("from_lists".into(), Ty::function(
105 vec![Ty::List(Box::new(Ty::List(Box::new(Ty::float()))))],
106 EffectSet::empty(),
107 mat(),
108 ));
109 fields.insert("from_flat".into(), Ty::function(
110 vec![Ty::int(), Ty::int(), Ty::List(Box::new(Ty::float()))],
111 EffectSet::empty(),
112 mat(),
113 ));
114 // Accessors.
115 fields.insert("rows".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
116 fields.insert("cols".into(), Ty::function(vec![mat()], EffectSet::empty(), Ty::int()));
117 fields.insert("get".into(), Ty::function(
118 vec![mat(), Ty::int(), Ty::int()], EffectSet::empty(), Ty::float(),
119 ));
120 fields.insert("to_flat".into(), Ty::function(
121 vec![mat()], EffectSet::empty(),
122 Ty::List(Box::new(Ty::float())),
123 ));
124 // Linalg ops.
125 fields.insert("transpose".into(), Ty::function(
126 vec![mat()], EffectSet::empty(), mat(),
127 ));
128 fields.insert("matmul".into(), Ty::function(
129 vec![mat(), mat()], EffectSet::empty(), mat(),
130 ));
131 fields.insert("scale".into(), Ty::function(
132 vec![Ty::float(), mat()], EffectSet::empty(), mat(),
133 ));
134 for name in &["add", "sub"] {
135 fields.insert((*name).into(), Ty::function(
136 vec![mat(), mat()], EffectSet::empty(), mat(),
137 ));
138 }
139 fields.insert("sigmoid".into(), Ty::function(
140 vec![mat()], EffectSet::empty(), mat(),
141 ));
142 Some(Ty::Record(fields))
143 }
144 "float" => {
145 let mut fields = IndexMap::new();
146 fields.insert("to_int".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::int()));
147 fields.insert("to_str".into(), Ty::function(vec![Ty::float()], EffectSet::empty(), Ty::str()));
148 Some(Ty::Record(fields))
149 }
150 "bytes" => {
151 let mut fields = IndexMap::new();
152 fields.insert("len".into(), Ty::function(
153 vec![Ty::bytes()], EffectSet::empty(), Ty::int(),
154 ));
155 fields.insert("is_empty".into(), Ty::function(
156 vec![Ty::bytes()], EffectSet::empty(), Ty::bool(),
157 ));
158 fields.insert("eq".into(), Ty::function(
159 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool(),
160 ));
161 fields.insert("from_str".into(), Ty::function(
162 vec![Ty::str()], EffectSet::empty(), Ty::bytes(),
163 ));
164 fields.insert("to_str".into(), Ty::function(
165 vec![Ty::bytes()], EffectSet::empty(),
166 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
167 ));
168 fields.insert("slice".into(), Ty::function(
169 vec![Ty::bytes(), Ty::int(), Ty::int()],
170 EffectSet::empty(), Ty::bytes(),
171 ));
172 // concat/concat_all + fixed-width little-endian int encoders and
173 // decoders: the minimal primitive set needed to build/parse a
174 // binary wire format (e.g. a Solana transaction) from Lex without
175 // shelling out to another language's runtime. Round-tripping
176 // through Str (the only prior way to build a Bytes buffer) can't
177 // represent arbitrary byte sequences, so these were a hard
178 // blocker, not a nice-to-have.
179 fields.insert("concat".into(), Ty::function(
180 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bytes(),
181 ));
182 fields.insert("concat_all".into(), Ty::function(
183 vec![Ty::List(Box::new(Ty::bytes()))], EffectSet::empty(), Ty::bytes(),
184 ));
185 fields.insert("u8".into(), Ty::function(
186 vec![Ty::int()], EffectSet::empty(), Ty::bytes(),
187 ));
188 fields.insert("u16_le".into(), Ty::function(
189 vec![Ty::int()], EffectSet::empty(), Ty::bytes(),
190 ));
191 fields.insert("u32_le".into(), Ty::function(
192 vec![Ty::int()], EffectSet::empty(), Ty::bytes(),
193 ));
194 fields.insert("u64_le".into(), Ty::function(
195 vec![Ty::int()], EffectSet::empty(), Ty::bytes(),
196 ));
197 fields.insert("u8_at".into(), Ty::function(
198 vec![Ty::bytes(), Ty::int()], EffectSet::empty(),
199 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
200 ));
201 fields.insert("u16_le_at".into(), Ty::function(
202 vec![Ty::bytes(), Ty::int()], EffectSet::empty(),
203 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
204 ));
205 fields.insert("u32_le_at".into(), Ty::function(
206 vec![Ty::bytes(), Ty::int()], EffectSet::empty(),
207 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
208 ));
209 fields.insert("u64_le_at".into(), Ty::function(
210 vec![Ty::bytes(), Ty::int()], EffectSet::empty(),
211 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
212 ));
213 Some(Ty::Record(fields))
214 }
215 "time" => {
216 // time.now() -> [time] Int — unix timestamp seconds.
217 // Reading the clock is an effect for two reasons: it's
218 // non-deterministic (replay needs the captured value) and
219 // it's a side-channel surface (see "Capability ≠
220 // correctness" on the landing page).
221 let mut fields = IndexMap::new();
222 fields.insert("now".into(), Ty::function(
223 vec![],
224 EffectSet::singleton("time"),
225 Ty::int(),
226 ));
227 // now_ms :: () -> [time] Int — unix milliseconds (#378).
228 // Resolution beyond what `time.now` (seconds) offers, for
229 // request-latency measurement / rate-limiter windows.
230 // Honors `LEX_TEST_NOW` for deterministic tests.
231 fields.insert("now_ms".into(), Ty::function(
232 vec![],
233 EffectSet::singleton("time"),
234 Ty::int(),
235 ));
236 // now_str :: () -> [time] Str — wall-clock instant rendered
237 // as an ISO-8601 / RFC 3339 string in UTC (#378). Suitable
238 // for auto-managed `created_at` / `updated_at` timestamps
239 // and structured log lines. Honors `LEX_TEST_NOW`.
240 fields.insert("now_str".into(), Ty::function(
241 vec![],
242 EffectSet::singleton("time"),
243 Ty::str(),
244 ));
245 // mono_ns :: () -> [time] Int — monotonic-clock nanoseconds
246 // since process start (#378). Use for *duration*
247 // measurement (`end - start`); the value carries no wall-
248 // clock meaning and the clock can never go backwards
249 // (unlike `time.now_ms` under NTP jitter). Not affected by
250 // `LEX_TEST_NOW` — pinning a monotonic clock would defeat
251 // its purpose; tests that need a fake monotonic clock
252 // should inject one through `EffectHandler`.
253 fields.insert("mono_ns".into(), Ty::function(
254 vec![],
255 EffectSet::singleton("time"),
256 Ty::int(),
257 ));
258 // sleep_ms :: Int -> [time] Unit (#226).
259 // Used internally by flow.retry_with_backoff for
260 // exponential-backoff delays; also available to user
261 // code under `--allow-effects time`.
262 fields.insert("sleep_ms".into(), Ty::function(
263 vec![Ty::int()],
264 EffectSet::singleton("time"),
265 Ty::Unit,
266 ));
267 // sleep :: Duration -> [time] Unit (#445).
268 // Duration-typed sleep — pairs with the
269 // `datetime.duration_seconds` / `duration_minutes` /
270 // `duration_days` constructors so periodic-task code
271 // expresses the period in units of meaning rather than
272 // raw milliseconds. Backed by `std::thread::sleep` at
273 // runtime — blocks the calling thread, which is the right
274 // semantics for the agent-driven workloads this exists
275 // for. Inside a `net.serve` worker the same caveat as
276 // `LEX_NET_INLINE_VM=1` applies (worker is stalled for `d`).
277 fields.insert("sleep".into(), Ty::function(
278 vec![Ty::Con("Duration".into(), vec![])],
279 EffectSet::singleton("time"),
280 Ty::Unit,
281 ));
282 Some(Ty::Record(fields))
283 }
284 "rand" => {
285 // rand.int_in(lo, hi) -> [random] Int — honest uniform draw in
286 // [lo, hi] (inclusive) from the OS RNG (#677). Carries the same
287 // `[random]` effect as `crypto.random` rather than a separate
288 // `rand` effect, so a reviewer auditing `--effect random` sees
289 // every non-deterministic draw in one place. For deterministic,
290 // replayable randomness thread a seed through `std.random`
291 // instead; for cryptographic strength use `crypto.random`.
292 let mut fields = IndexMap::new();
293 fields.insert("int_in".into(), Ty::function(
294 vec![Ty::int(), Ty::int()],
295 EffectSet::singleton("random"),
296 Ty::int(),
297 ));
298 Some(Ty::Record(fields))
299 }
300 "random" => {
301 // #219: pure, seeded RNG. The caller threads the `Rng`
302 // value through computations explicitly — there is no
303 // global state and no effect tag, because the seed is
304 // visible in the program's value flow and replay is
305 // therefore deterministic by construction.
306 //
307 // Backed at runtime by SplitMix64 (deterministic across
308 // platforms, single-u64 state). The proposal mentioned
309 // `rand_chacha` for cryptographic-strength bias, but the
310 // acceptance criterion is just "byte-identical sequence
311 // across platforms," and SplitMix64 satisfies that with
312 // a state shape that fits in `Value::Int` cleanly.
313 let rng_t = || Ty::Con("Rng".into(), vec![]);
314 let mut fields = IndexMap::new();
315 // seed :: Int -> Rng
316 fields.insert("seed".into(), Ty::function(
317 vec![Ty::int()], EffectSet::empty(), rng_t()));
318 // int :: Rng, Int, Int -> (Int, Rng)
319 // Uniform in [lo, hi] inclusive at both ends. Returns
320 // the drawn value and the advanced Rng.
321 fields.insert("int".into(), Ty::function(
322 vec![rng_t(), Ty::int(), Ty::int()],
323 EffectSet::empty(),
324 Ty::Tuple(vec![Ty::int(), rng_t()])));
325 // float :: Rng -> (Float, Rng)
326 // Uniform in [0.0, 1.0).
327 fields.insert("float".into(), Ty::function(
328 vec![rng_t()], EffectSet::empty(),
329 Ty::Tuple(vec![Ty::float(), rng_t()])));
330 // choose :: Rng, List[T] -> Option[(T, Rng)]
331 // Returns None if the list is empty.
332 fields.insert("choose".into(), Ty::function(
333 vec![rng_t(), Ty::List(Box::new(Ty::Var(0)))],
334 EffectSet::empty(),
335 Ty::Con("Option".into(), vec![
336 Ty::Tuple(vec![Ty::Var(0), rng_t()]),
337 ]),
338 ));
339 Some(Ty::Record(fields))
340 }
341 "env" => {
342 // #216: env.get(name) -> [env] Option[Str].
343 // Per-var scoping (`[env(NAME)]`) lands with the
344 // per-capability effect parameterization work (#207); the
345 // flat `[env]` is the v1 surface.
346 let mut fields = IndexMap::new();
347 fields.insert("get".into(), Ty::function(
348 vec![Ty::str()],
349 EffectSet::singleton("env"),
350 Ty::Con("Option".into(), vec![Ty::str()]),
351 ));
352 Some(Ty::Record(fields))
353 }
354 "net" => {
355 let mut fields = IndexMap::new();
356 // get :: Str -> [net] Result[Str, Str]
357 fields.insert("get".into(), Ty::function(
358 vec![Ty::str()],
359 EffectSet::singleton("net"),
360 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
361 ));
362 fields.insert("post".into(), Ty::function(
363 vec![Ty::str(), Ty::str()],
364 EffectSet::singleton("net"),
365 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
366 ));
367 // ── UDP datagrams (#760) ──────────────────────────────────
368 // Everything else in this module is a stream, and almost
369 // everything is HTTP. That leaves every protocol built on
370 // datagrams unreachable from Lex: Wake-on-LAN (a broadcast),
371 // NTP, mDNS/DNS-SD and SSDP discovery (multicast), MAVLink,
372 // and the device protocols behind most consumer hardware.
373 //
374 // Socket-handle shaped rather than one-shot, following
375 // `sql.open`: an Int handle into a runtime registry. A
376 // request/response helper would have covered the motivating
377 // case in one call, but not listening — mDNS and game traffic
378 // need a socket that outlives a single exchange.
379 //
380 // udp_open(port) -> Result[Int, Str]
381 // udp_close(sock) -> Result[Unit, Str]
382 // udp_send(sock, host, port, data) -> Result[Int, Str]
383 // udp_recv(sock, timeout_ms) -> Result[UdpDatagram, Str]
384 // udp_broadcast(sock, on) -> Result[Unit, Str]
385 // udp_join_multicast(sock, group) -> Result[Unit, Str]
386 //
387 // `udp_open(0)` binds an ephemeral port. `udp_send` returns the
388 // byte count written. `udp_recv` blocks up to `timeout_ms` and
389 // reports a timeout as `Err`, never as an empty datagram — an
390 // empty UDP payload is legal and must stay distinguishable from
391 // nothing having arrived.
392 //
393 // POLICY: `udp_send` honours `--allow-net-host` against the
394 // DESTINATION, exactly as `net.get` does against a URL's host.
395 // Without that a datagram socket would be a hole straight
396 // through the one gate the rest of this module respects.
397 // Broadcast and multicast addresses have to be named in the
398 // allowlist like any other destination, which is the intended
399 // friction: sending to 255.255.255.255 is a larger capability
400 // than sending to one known host, and should have to be asked
401 // for.
402 fields.insert("udp_open".into(), Ty::function(
403 vec![Ty::int()],
404 EffectSet::singleton("net"),
405 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
406 ));
407 fields.insert("udp_close".into(), Ty::function(
408 vec![Ty::int()],
409 EffectSet::singleton("net"),
410 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
411 ));
412 fields.insert("udp_send".into(), Ty::function(
413 vec![Ty::int(), Ty::str(), Ty::int(), Ty::bytes()],
414 EffectSet::singleton("net"),
415 Ty::Con("Result".into(), vec![Ty::int(), Ty::str()]),
416 ));
417 fields.insert("udp_recv".into(), Ty::function(
418 vec![Ty::int(), Ty::int()],
419 EffectSet::singleton("net"),
420 Ty::Con("Result".into(), vec![
421 Ty::Con("UdpDatagram".into(), vec![]), Ty::str(),
422 ]),
423 ));
424 fields.insert("udp_broadcast".into(), Ty::function(
425 vec![Ty::int(), Ty::bool()],
426 EffectSet::singleton("net"),
427 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
428 ));
429 fields.insert("udp_join_multicast".into(), Ty::function(
430 vec![Ty::int(), Ty::str()],
431 EffectSet::singleton("net"),
432 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
433 ));
434
435 // serve :: (Int, Str) -> [net] Unit (blocks; never returns
436 // under normal use). Handler's signature isn't carried in
437 // the type system here — looked up by name at runtime.
438 fields.insert("serve".into(), Ty::function(
439 vec![Ty::int(), Ty::str()],
440 EffectSet::singleton("net"),
441 Ty::Unit,
442 ));
443 // serve_tls :: (Int, Str, Str, Str) -> [net] Unit
444 // port cert key handler
445 // cert and key are filesystem paths to PEM-encoded files.
446 fields.insert("serve_tls".into(), Ty::function(
447 vec![Ty::int(), Ty::str(), Ty::str(), Ty::str()],
448 EffectSet::singleton("net"),
449 Ty::Unit,
450 ));
451 // serve_ws :: (Int, Str) -> [net] Unit
452 // port on_message_handler_name
453 // The handler is looked up by name at runtime.
454 fields.insert("serve_ws".into(), Ty::function(
455 vec![Ty::int(), Ty::str()],
456 EffectSet::singleton("net"),
457 Ty::Unit,
458 ));
459 // serve_ws_fn[Eff] :: (Int, Str, (WsConn, WsMessage) -> [Eff] WsAction)
460 // -> [net, Eff] Unit
461 // Effect-polymorphic WebSocket server that accepts a handler closure.
462 // The second argument is the subprotocol string for the
463 // Sec-WebSocket-Protocol handshake header ("" for none).
464 // open_var(0) propagates the handler's effect row to the call site.
465 fields.insert("serve_ws_fn".into(), Ty::function(
466 vec![
467 Ty::int(),
468 Ty::str(), // subprotocol
469 Ty::function(
470 vec![
471 Ty::Con("WsConn".into(), vec![]),
472 Ty::Con("WsMessage".into(), vec![]),
473 ],
474 EffectSet::open_var(0),
475 Ty::Con("WsAction".into(), vec![]),
476 ),
477 ],
478 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
479 Ty::Unit,
480 ));
481 // serve_ws_fn_auth[Eff] :: (Int, Str,
482 // (Str, List[{name :: Str, value :: Str}]) -> [Eff] Result[Unit, Str],
483 // (WsConn, WsMessage) -> [Eff] WsAction)
484 // -> [net, Eff] Unit
485 // Variant of serve_ws_fn that runs a pre-handshake auth
486 // callback against the upgrade request's path + headers.
487 // `Err(msg)` from the callback responds 401 Unauthorized
488 // and skips the WS upgrade entirely (#423). The auth and
489 // message-handler closures share the same effect row, so
490 // a caller using e.g. `[sql]` to look up a password hash
491 // in auth can use `[sql]` in subsequent handlers without
492 // duplicating the declaration.
493 let header_entry = || {
494 let mut fs = IndexMap::new();
495 fs.insert("name".into(), Ty::str());
496 fs.insert("value".into(), Ty::str());
497 Ty::Record(fs)
498 };
499 fields.insert("serve_ws_fn_auth".into(), Ty::function(
500 vec![
501 Ty::int(),
502 Ty::str(), // subprotocol
503 // auth callback: (path, headers) -> [Eff] Result[Unit, Str]
504 Ty::function(
505 vec![
506 Ty::str(),
507 Ty::List(Box::new(header_entry())),
508 ],
509 EffectSet::open_var(0),
510 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
511 ),
512 // on_message: same shape as serve_ws_fn
513 Ty::function(
514 vec![
515 Ty::Con("WsConn".into(), vec![]),
516 Ty::Con("WsMessage".into(), vec![]),
517 ],
518 EffectSet::open_var(0),
519 Ty::Con("WsAction".into(), vec![]),
520 ),
521 ],
522 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
523 Ty::Unit,
524 ));
525 // serve_ws_fn_actor[Eff] ::
526 // (Int, Str,
527 // (WsConn) -> Str, # name_of, registry name
528 // (WsConn, WsMessage) -> [Eff] WsAction) # on_message
529 // -> [net, concurrent, Eff] Unit
530 //
531 // Variant of serve_ws_fn that registers each accepted
532 // connection as a named actor in conc_registry. Non-WS
533 // callers can then `conc.lookup(name) |> conc.tell(frame)`
534 // to push outbound frames into the socket from arbitrary
535 // [concurrent]-tagged code (HTTP webhooks, scheduled tasks,
536 // broadcast loops). Documented in #459.
537 //
538 // name_of is intentionally pure: it inspects the WsConn
539 // record (id / path / subprotocol) and decides what
540 // name to register the connection under. Empty string
541 // means "don't register this connection" — `on_message`
542 // still runs but no outbound handle is exposed.
543 //
544 // The result row carries `concurrent` because the runtime
545 // registers an `ActorHandler::Native` bridge in the conc
546 // registry; lookups from non-WS callers are themselves
547 // `[concurrent]` effects.
548 // serve_ws_fn_actor_with[Eff] ::
549 // (Int, Str,
550 // (WsConn) -> Str,
551 // (WsConn, WsMessage) -> [Eff] WsAction,
552 // ServeOpts)
553 // -> [net, concurrent, Eff] Unit
554 //
555 // `serve_ws_fn_actor` with the bind interface named in the
556 // source (#719). Every WS server bound `127.0.0.1`
557 // unconditionally, so a containerised deployment could not
558 // accept cross-container connections without a `socat`
559 // sidecar republishing the loopback listener.
560 //
561 // The opts record is the one `net.default_opts()` already
562 // returns, per the issue's own suggestion — only `host` is
563 // read, since `http2` and `inline_vm` describe an HTTP
564 // server and mean nothing to a websocket listener. One
565 // `ServeOpts` in the language beats two that differ by two
566 // fields.
567 //
568 // The other three WS servers honour `LEX_WS_HOST` but have
569 // no `_with` variant yet; adding one is a hand-written
570 // builtin today, and `std.net` has not migrated to the
571 // declarative catalogue (#778) where it would be a row.
572 fields.insert("serve_ws_fn_actor_with".into(), Ty::function(
573 vec![
574 Ty::int(),
575 Ty::str(), // subprotocol
576 Ty::function(
577 vec![Ty::Con("WsConn".into(), vec![])],
578 EffectSet::empty(),
579 Ty::str(),
580 ),
581 Ty::function(
582 vec![
583 Ty::Con("WsConn".into(), vec![]),
584 Ty::Con("WsMessage".into(), vec![]),
585 ],
586 EffectSet::open_var(0),
587 Ty::Con("WsAction".into(), vec![]),
588 ),
589 {
590 // The same shape `serve_opts_t()` builds below;
591 // spelled out here because that helper is
592 // declared further down this arm.
593 let mut fs = IndexMap::new();
594 fs.insert("http2".into(), Ty::bool());
595 fs.insert("inline_vm".into(), Ty::bool());
596 fs.insert("host".into(), Ty::str());
597 Ty::Record(fs)
598 },
599 ],
600 EffectSet::open_var(0)
601 .union(&EffectSet::singleton("net"))
602 .union(&EffectSet::singleton("concurrent")),
603 Ty::Unit,
604 ));
605 fields.insert("serve_ws_fn_actor".into(), Ty::function(
606 vec![
607 Ty::int(),
608 Ty::str(), // subprotocol
609 Ty::function(
610 vec![Ty::Con("WsConn".into(), vec![])],
611 EffectSet::empty(),
612 Ty::str(),
613 ),
614 Ty::function(
615 vec![
616 Ty::Con("WsConn".into(), vec![]),
617 Ty::Con("WsMessage".into(), vec![]),
618 ],
619 EffectSet::open_var(0),
620 Ty::Con("WsAction".into(), vec![]),
621 ),
622 ],
623 EffectSet::open_var(0)
624 .union(&EffectSet::singleton("net"))
625 .union(&EffectSet::singleton("concurrent")),
626 Ty::Unit,
627 ));
628 // dial_ws[Eff] :: (Str, Str, () -> [Eff] WsAction,
629 // (WsMessage) -> [Eff] WsAction)
630 // -> [net, Eff] Result[Unit, Str]
631 //
632 // WebSocket *client* — the inverse of serve_ws_fn (#390).
633 // Connects to `url` (ws:// or wss://) with the given
634 // subprotocol header, calls `on_open` once after the
635 // handshake completes, then loops invoking `on_message`
636 // for every inbound frame. Each callback returns a
637 // `WsAction` that gets applied to the socket — same enum
638 // as the server side, same semantics for `WsSend` /
639 // `WsSendBinary` / `WsNoOp`. open_var(0) propagates the
640 // handler effects so callers that touch [io], [time],
641 // [random] etc. inside their handlers see those propagate
642 // out of the dial_ws call.
643 //
644 // Returns `Result[Unit, Str]` rather than the bare `Unit`
645 // that serve_ws_fn returns: a dial can fail on connect
646 // (DNS, refused, bad TLS) or mid-stream (read error,
647 // unexpected close) and the caller usually wants to know.
648 fields.insert("dial_ws".into(), Ty::function(
649 vec![
650 Ty::str(), // url (ws:// or wss://)
651 Ty::str(), // subprotocol (Sec-WebSocket-Protocol)
652 Ty::function(
653 vec![],
654 EffectSet::open_var(0),
655 Ty::Con("WsAction".into(), vec![]),
656 ),
657 Ty::function(
658 vec![Ty::Con("WsMessage".into(), vec![])],
659 EffectSet::open_var(0),
660 Ty::Con("WsAction".into(), vec![]),
661 ),
662 ],
663 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
664 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
665 ));
666 // dial_ws_actor[Eff] :: (Str, Str, Str,
667 // () -> [Eff] WsAction,
668 // (WsMessage) -> [Eff] WsAction)
669 // -> [net, Eff] Result[Unit, Str]
670 //
671 // Variant of dial_ws that registers the outgoing connection in the
672 // conc registry under `name`. conc.tell(actor, frame_str) enqueues
673 // a frame for delivery, enabling proactive sends (heartbeats,
674 // meter values) from any other actor without changing the
675 // reactive on_message signature.
676 fields.insert("dial_ws_actor".into(), Ty::function(
677 vec![
678 Ty::str(), // url
679 Ty::str(), // subprotocol ("" for none)
680 Ty::str(), // conc registry name ("" to skip registration)
681 Ty::function(
682 vec![],
683 EffectSet::open_var(0),
684 Ty::Con("WsAction".into(), vec![]),
685 ),
686 Ty::function(
687 vec![Ty::Con("WsMessage".into(), vec![])],
688 EffectSet::open_var(0),
689 Ty::Con("WsAction".into(), vec![]),
690 ),
691 ],
692 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
693 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]),
694 ));
695 // serve_fn[Eff] :: (Int, (Request) -> [Eff] Response) -> [net, Eff] Unit
696 // Effect-polymorphic variant of serve that accepts a first-class closure
697 // instead of a handler name. open_var(0) captures the handler's effect row
698 // so callers that invoke e.g. [io] effects inside the closure propagate them
699 // to the serve_fn call site.
700 fields.insert("serve_fn".into(), Ty::function(
701 vec![
702 Ty::int(),
703 Ty::function(
704 vec![Ty::Con("Request".into(), vec![])],
705 EffectSet::open_var(0),
706 Ty::Con("Response".into(), vec![]),
707 ),
708 ],
709 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
710 Ty::Unit,
711 ));
712 // serve_routed[Eff] :: (
713 // Int,
714 // List[(Str, Str, (Request) -> [Eff] Response)],
715 // (Request) -> [Eff] Response
716 // ) -> [net, Eff] Unit
717 //
718 // Pattern-matched dispatch over `serve_fn`. Each route is a
719 // (method, path-pattern, handler) triple — method is an
720 // HTTP verb (case-insensitive) or "*" for any; path-patterns
721 // use `:name` segments (e.g. "/users/:id") and matched values
722 // are stamped onto `req.path_params` before the handler runs.
723 // Routes are tried in registration order; the first match
724 // wins. `fallback` runs when no route matches — typically a
725 // 404 responder. Same `open_var(0)` effect-row trick as
726 // `serve_fn` so handler effects propagate to the call site.
727 fields.insert("serve_routed".into(), Ty::function(
728 vec![
729 Ty::int(),
730 Ty::List(Box::new(Ty::Tuple(vec![
731 Ty::str(),
732 Ty::str(),
733 Ty::function(
734 vec![Ty::Con("Request".into(), vec![])],
735 EffectSet::open_var(0),
736 Ty::Con("Response".into(), vec![]),
737 ),
738 ]))),
739 Ty::function(
740 vec![Ty::Con("Request".into(), vec![])],
741 EffectSet::open_var(0),
742 Ty::Con("Response".into(), vec![]),
743 ),
744 ],
745 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
746 Ty::Unit,
747 ));
748
749 // ServeOpts is a structural record literal — callers build
750 // it with `{ http2: ..., inline_vm: ..., host: ... }`. Used
751 // by `serve_with` / `serve_fn_with` / `serve_routed_with`
752 // to replace the legacy LEX_NET_HTTP2 / LEX_NET_INLINE_VM
753 // env-var gates with a first-class, type-checked config.
754 // See lex-lang#497.
755 let serve_opts_t = || {
756 let mut fs = IndexMap::new();
757 fs.insert("http2".into(), Ty::bool());
758 fs.insert("inline_vm".into(), Ty::bool());
759 fs.insert("host".into(), Ty::str());
760 Ty::Record(fs)
761 };
762
763 // default_opts :: () -> ServeOpts
764 // Returns the same defaults the legacy serve* paths use —
765 // http2=false, inline_vm=false, host="0.0.0.0". Pure; the
766 // env-var fallback only applies on the legacy serve* path,
767 // not here.
768 fields.insert("default_opts".into(), Ty::function(
769 vec![],
770 EffectSet::empty(),
771 serve_opts_t(),
772 ));
773
774 // serve_with :: (Int, Str, ServeOpts) -> [net] Unit
775 fields.insert("serve_with".into(), Ty::function(
776 vec![Ty::int(), Ty::str(), serve_opts_t()],
777 EffectSet::singleton("net"),
778 Ty::Unit,
779 ));
780
781 // serve_fn_with[Eff] :: (Int, (Request) -> [Eff] Response, ServeOpts)
782 // -> [net, Eff] Unit
783 fields.insert("serve_fn_with".into(), Ty::function(
784 vec![
785 Ty::int(),
786 Ty::function(
787 vec![Ty::Con("Request".into(), vec![])],
788 EffectSet::open_var(0),
789 Ty::Con("Response".into(), vec![]),
790 ),
791 serve_opts_t(),
792 ],
793 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
794 Ty::Unit,
795 ));
796
797 // serve_routed_with[Eff] :: (
798 // Int, List[(Str, Str, (Request) -> [Eff] Response)],
799 // (Request) -> [Eff] Response, ServeOpts
800 // ) -> [net, Eff] Unit
801 fields.insert("serve_routed_with".into(), Ty::function(
802 vec![
803 Ty::int(),
804 Ty::List(Box::new(Ty::Tuple(vec![
805 Ty::str(),
806 Ty::str(),
807 Ty::function(
808 vec![Ty::Con("Request".into(), vec![])],
809 EffectSet::open_var(0),
810 Ty::Con("Response".into(), vec![]),
811 ),
812 ]))),
813 Ty::function(
814 vec![Ty::Con("Request".into(), vec![])],
815 EffectSet::open_var(0),
816 Ty::Con("Response".into(), vec![]),
817 ),
818 serve_opts_t(),
819 ],
820 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
821 Ty::Unit,
822 ));
823
824 // serve_quic / serve_quic_fn / serve_quic_routed (#496).
825 // HTTP/3 over QUIC. TlsConfig is an opaque value built by
826 // `tls.from_pem_files` or `tls.self_signed` — it carries the
827 // server certificate chain + private key needed for the
828 // QUIC handshake (TLS is mandatory for HTTP/3). Effect row
829 // stays `[net]` for symmetry with `serve` / `serve_fn`;
830 // policy gates don't distinguish HTTP/1.1+2 (TCP) from
831 // HTTP/3 (UDP) at the effect level.
832 //
833 // serve_quic :: (Int, TlsConfig, Str) -> [net] Unit
834 fields.insert("serve_quic".into(), Ty::function(
835 vec![Ty::int(), Ty::Con("TlsConfig".into(), vec![]), Ty::str()],
836 EffectSet::singleton("net"),
837 Ty::Unit,
838 ));
839
840 // serve_quic_fn[Eff] :: (Int, TlsConfig,
841 // (Request) -> [Eff] Response)
842 // -> [net, Eff] Unit
843 fields.insert("serve_quic_fn".into(), Ty::function(
844 vec![
845 Ty::int(),
846 Ty::Con("TlsConfig".into(), vec![]),
847 Ty::function(
848 vec![Ty::Con("Request".into(), vec![])],
849 EffectSet::open_var(0),
850 Ty::Con("Response".into(), vec![]),
851 ),
852 ],
853 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
854 Ty::Unit,
855 ));
856
857 // serve_quic_routed[Eff] :: (
858 // Int, TlsConfig,
859 // List[(Str, Str, (Request) -> [Eff] Response)],
860 // (Request) -> [Eff] Response
861 // ) -> [net, Eff] Unit
862 fields.insert("serve_quic_routed".into(), Ty::function(
863 vec![
864 Ty::int(),
865 Ty::Con("TlsConfig".into(), vec![]),
866 Ty::List(Box::new(Ty::Tuple(vec![
867 Ty::str(),
868 Ty::str(),
869 Ty::function(
870 vec![Ty::Con("Request".into(), vec![])],
871 EffectSet::open_var(0),
872 Ty::Con("Response".into(), vec![]),
873 ),
874 ]))),
875 Ty::function(
876 vec![Ty::Con("Request".into(), vec![])],
877 EffectSet::open_var(0),
878 Ty::Con("Response".into(), vec![]),
879 ),
880 ],
881 EffectSet::open_var(0).union(&EffectSet::singleton("net")),
882 Ty::Unit,
883 ));
884
885 Some(Ty::Record(fields))
886 }
887 // `tls` — TLS certificate handling for `net.serve_quic` (#496).
888 // `TlsConfig` is opaque to user code; the only ways to obtain
889 // one are these constructors. The runtime keeps the certificate
890 // chain + private key behind that opaque type so we can change
891 // the internal representation (record-of-bytes today, possibly
892 // a Resource handle tomorrow) without breaking source code.
893 "tls" => {
894 let mut fields = IndexMap::new();
895 // from_pem_files :: (Str, Str) -> [fs_read] Result[TlsConfig, Str]
896 // cert key
897 // Load a PEM-encoded certificate chain + private key from
898 // disk. Both paths are read with the `[fs_read]` effect so
899 // policy gates can restrict where certs may come from.
900 fields.insert("from_pem_files".into(), Ty::function(
901 vec![Ty::str(), Ty::str()],
902 EffectSet::singleton("fs_read"),
903 Ty::Con("Result".into(), vec![
904 Ty::Con("TlsConfig".into(), vec![]),
905 Ty::str(),
906 ]),
907 ));
908 // self_signed :: Str -> Result[TlsConfig, Str]
909 // Generate a self-signed certificate for the given hostname
910 // (or "localhost"). Pure — no effects needed. Intended for
911 // local development and integration tests only; real
912 // deployments should use a CA-signed cert via from_pem_files.
913 fields.insert("self_signed".into(), Ty::function(
914 vec![Ty::str()],
915 EffectSet::empty(),
916 Ty::Con("Result".into(), vec![
917 Ty::Con("TlsConfig".into(), vec![]),
918 Ty::str(),
919 ]),
920 ));
921 Some(Ty::Record(fields))
922 }
923 "chat" => {
924 let mut fields = IndexMap::new();
925 fields.insert("broadcast".into(), Ty::function(
926 vec![Ty::str(), Ty::str()],
927 EffectSet::singleton("chat"),
928 Ty::Unit,
929 ));
930 fields.insert("send".into(), Ty::function(
931 vec![Ty::int(), Ty::str()],
932 EffectSet::singleton("chat"),
933 Ty::bool(),
934 ));
935 Some(Ty::Record(fields))
936 }
937 "conc" => {
938 // Actor model (#381). Effect: [concurrent].
939 // spawn :: S, (S, M) -> [E] (S, R) -> [concurrent] Actor[S]
940 // ask :: Actor[S], M -> [concurrent] R
941 // tell :: Actor[S], M -> [concurrent] Unit
942 //
943 // The type variables used here are fresh placeholders;
944 // the checker instantiates them at each call site.
945 // 0 = S (state), 1 = M (message), 2 = R (reply), 3 = E (effect row)
946 let actor_t = |s: Ty| Ty::Con("Actor".into(), vec![s]);
947 let mut fields = IndexMap::new();
948 // spawn :: S, (S, M -> [E] (S, R)) -> [concurrent] Actor[S]
949 fields.insert("spawn".into(), Ty::function(
950 vec![
951 Ty::Var(0),
952 Ty::Function {
953 params: vec![Ty::Var(0), Ty::Var(1)],
954 effects: EffectSet::open_var(3),
955 ret: Box::new(Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])),
956 },
957 ],
958 EffectSet::singleton("concurrent"),
959 actor_t(Ty::Var(0)),
960 ));
961 // ask :: Actor[S], M -> [concurrent] R
962 fields.insert("ask".into(), Ty::function(
963 vec![actor_t(Ty::Var(0)), Ty::Var(1)],
964 EffectSet::singleton("concurrent"),
965 Ty::Var(2),
966 ));
967 // tell :: Actor[S], M -> [concurrent] Unit
968 fields.insert("tell".into(), Ty::function(
969 vec![actor_t(Ty::Var(0)), Ty::Var(1)],
970 EffectSet::singleton("concurrent"),
971 Ty::Unit,
972 ));
973 // #444 — named-actor discovery within a process.
974 //
975 // register :: Actor[S], Str -> [concurrent] Result[Unit, ConcError]
976 // Returns Err(AlreadyRegistered(name)) if the name is
977 // taken — registration is exclusive so name collisions
978 // surface at the source level, not as silent overwrites.
979 //
980 // lookup :: Str -> [concurrent] Option[Actor[S]]
981 // Returns Some(actor) if registered, None otherwise. The
982 // static `[S]` parametrisation isn't checked at runtime in
983 // v1; the caller is responsible for matching the
984 // registration site's type. SigId-tagged variant deferred —
985 // see `conc_registry.rs` in lex-bytecode.
986 //
987 // unregister :: Str -> [concurrent] Result[Unit, ConcError]
988 // Returns Err(NotRegistered(name)) if absent. Existing
989 // `Actor[S]` handles held by callers continue to work
990 // after unregistration; the cell is reclaimed when the
991 // last handle drops.
992 //
993 // registered :: () -> [concurrent] List[Str]
994 // Sorted snapshot of currently registered names. Debug /
995 // introspection — not part of the steady-state agent flow.
996 let conc_err = || Ty::Con("ConcError".into(), vec![]);
997 let result_ce = |ok: Ty| Ty::Con("Result".into(), vec![ok, conc_err()]);
998 fields.insert("register".into(), Ty::function(
999 vec![actor_t(Ty::Var(0)), Ty::str()],
1000 EffectSet::singleton("concurrent"),
1001 result_ce(Ty::Unit),
1002 ));
1003 fields.insert("lookup".into(), Ty::function(
1004 vec![Ty::str()],
1005 EffectSet::singleton("concurrent"),
1006 Ty::Con("Option".into(), vec![actor_t(Ty::Var(0))]),
1007 ));
1008 fields.insert("unregister".into(), Ty::function(
1009 vec![Ty::str()],
1010 EffectSet::singleton("concurrent"),
1011 result_ce(Ty::Unit),
1012 ));
1013 fields.insert("registered".into(), Ty::function(
1014 vec![],
1015 EffectSet::singleton("concurrent"),
1016 Ty::List(Box::new(Ty::str())),
1017 ));
1018 Some(Ty::Record(fields))
1019 }
1020 "arrow" => {
1021 // Apache Arrow tables (#426). All ops are pure (no effects);
1022 // tables are immutable and conversions / reductions all run as
1023 // one Rust call over the flat buffer.
1024 //
1025 // `arrow.Table` is opaque from the type system's point of view —
1026 // the runtime variant `Value::ArrowTable` is the only producer
1027 // and consumer, so we model it as a 0-arity type constructor.
1028 let table = Ty::Con("Table".into(), vec![]);
1029 let str_t = Ty::str();
1030 let int_t = Ty::int();
1031 let float_t = Ty::float();
1032 let opt = |inner: Ty| Ty::Con("Option".into(), vec![inner]);
1033 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
1034 let no_eff = EffectSet::empty();
1035
1036 let mut fields = IndexMap::new();
1037
1038 // -- constructors --
1039 // arrow.from_int_columns :: List[(Str, List[Int])] -> Result[Table, Str]
1040 // arrow.from_float_columns :: List[(Str, List[Float])] -> Result[Table, Str]
1041 // arrow.from_str_columns :: List[(Str, List[Str])] -> Result[Table, Str]
1042 for (name, elem) in [
1043 ("from_int_columns", int_t.clone()),
1044 ("from_float_columns", float_t.clone()),
1045 ("from_str_columns", str_t.clone()),
1046 ] {
1047 fields.insert(name.into(), Ty::function(
1048 vec![Ty::List(Box::new(Ty::Tuple(vec![
1049 str_t.clone(),
1050 Ty::List(Box::new(elem)),
1051 ])))],
1052 no_eff.clone(),
1053 res(table.clone()),
1054 ));
1055 }
1056
1057 // -- introspection --
1058 // arrow.nrows / arrow.ncols :: Table -> Int
1059 fields.insert("nrows".into(), Ty::function(
1060 vec![table.clone()], no_eff.clone(), int_t.clone()));
1061 fields.insert("ncols".into(), Ty::function(
1062 vec![table.clone()], no_eff.clone(), int_t.clone()));
1063 // arrow.col_names :: Table -> List[Str]
1064 fields.insert("col_names".into(), Ty::function(
1065 vec![table.clone()], no_eff.clone(),
1066 Ty::List(Box::new(str_t.clone()))));
1067 // arrow.col_type :: Table, Str -> Option[Str]
1068 fields.insert("col_type".into(), Ty::function(
1069 vec![table.clone(), str_t.clone()],
1070 no_eff.clone(), opt(str_t.clone())));
1071
1072 // -- column reductions --
1073 // arrow.col_sum_int :: Table, Str -> Result[Int, Str]
1074 // arrow.col_sum_float :: Table, Str -> Result[Float, Str]
1075 // arrow.col_mean :: Table, Str -> Result[Option[Float], Str]
1076 // arrow.col_min_int :: Table, Str -> Result[Option[Int], Str]
1077 // arrow.col_max_int :: Table, Str -> Result[Option[Int], Str]
1078 // arrow.col_count :: Table, Str -> Result[Int, Str]
1079 for (name, ret_ok) in [
1080 ("col_sum_int", int_t.clone()),
1081 ("col_sum_float", float_t.clone()),
1082 ("col_mean", opt(float_t.clone())),
1083 ("col_min_int", opt(int_t.clone())),
1084 ("col_max_int", opt(int_t.clone())),
1085 ("col_count", int_t.clone()),
1086 ] {
1087 fields.insert(name.into(), Ty::function(
1088 vec![table.clone(), str_t.clone()],
1089 no_eff.clone(), res(ret_ok)));
1090 }
1091
1092 // -- slicing --
1093 // arrow.head / tail :: Table, Int -> Table
1094 for name in &["head", "tail"] {
1095 fields.insert((*name).into(), Ty::function(
1096 vec![table.clone(), int_t.clone()],
1097 no_eff.clone(), table.clone()));
1098 }
1099 // arrow.slice :: Table, Int, Int -> Table
1100 fields.insert("slice".into(), Ty::function(
1101 vec![table.clone(), int_t.clone(), int_t.clone()],
1102 no_eff.clone(), table.clone()));
1103 // arrow.select_cols :: Table, List[Str] -> Result[Table, Str]
1104 fields.insert("select_cols".into(), Ty::function(
1105 vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
1106 no_eff.clone(), res(table.clone())));
1107 // arrow.drop_col :: Table, Str -> Result[Table, Str]
1108 fields.insert("drop_col".into(), Ty::function(
1109 vec![table.clone(), str_t.clone()],
1110 no_eff.clone(), res(table.clone())));
1111 // arrow.rename_col :: Table, Str, Str -> Result[Table, Str]
1112 // Schema-only rename (old name, new name); zero-copy — the
1113 // underlying column arrays are untouched.
1114 fields.insert("rename_col".into(), Ty::function(
1115 vec![table.clone(), str_t.clone(), str_t.clone()],
1116 no_eff.clone(), res(table.clone())));
1117
1118 // -- I/O (effect-gated) --
1119 // arrow.read_csv :: Str -> [fs_read] Result[Table, Str]
1120 // Header row required; schema inferred from the first 100 rows.
1121 // The `[fs_read]` effect surfaces in agent-tool policy gates
1122 // and `--allow-fs-read` per-path scoping, same as `io.read`.
1123 fields.insert("read_csv".into(), Ty::function(
1124 vec![str_t.clone()],
1125 EffectSet::singleton("fs_read"),
1126 res(table.clone())));
1127
1128 // arrow.read_parquet :: Str -> [fs_read] Result[Table, Str]
1129 // arrow.read_parquet_cols :: (Str, List[Str]) -> [fs_read] Result[Table, Str]
1130 // Same effect + path-scope rules as read_csv. _cols pushes
1131 // the projection into the Parquet reader (no decode of skipped
1132 // columns); missing column names surface as Err.
1133 fields.insert("read_parquet".into(), Ty::function(
1134 vec![str_t.clone()],
1135 EffectSet::singleton("fs_read"),
1136 res(table.clone())));
1137 fields.insert("read_parquet_cols".into(), Ty::function(
1138 vec![str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1139 EffectSet::singleton("fs_read"),
1140 res(table.clone())));
1141
1142 // arrow.write_parquet :: (Table, Str) -> [fs_write] Result[Unit, Str]
1143 // arrow.write_csv :: (Table, Str) -> [fs_write] Result[Unit, Str]
1144 // Path scope uses --allow-fs-write (symmetric with io.write).
1145 // Parquet default: Snappy compression, default page/row-group
1146 // sizes — sufficient for v1; a write_parquet_opts variant
1147 // can ride a later issue if knobs are needed.
1148 for name in &["write_parquet", "write_csv"] {
1149 fields.insert((*name).into(), Ty::function(
1150 vec![table.clone(), str_t.clone()],
1151 EffectSet::singleton("fs_write"),
1152 res(Ty::Unit)));
1153 }
1154
1155 Some(Ty::Record(fields))
1156 }
1157 "df" => {
1158 // Polars-backed query ops over arrow.Table (#427). All pure
1159 // (no effects); the Polars DataFrame is internal plumbing,
1160 // never leaves the kernel.
1161 let table = Ty::Con("Table".into(), vec![]);
1162 let str_t = Ty::str();
1163 let int_t = Ty::int();
1164 let float_t = Ty::float();
1165 let bool_t = Ty::bool();
1166 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
1167 let no_eff = EffectSet::empty();
1168
1169 let mut fields = IndexMap::new();
1170
1171 // df.filter_{eq,gt,lt}_int :: Table, Str, Int -> Result[Table, Str]
1172 for name in &["filter_eq_int", "filter_gt_int", "filter_lt_int"] {
1173 fields.insert((*name).into(), Ty::function(
1174 vec![table.clone(), str_t.clone(), int_t.clone()],
1175 no_eff.clone(), res(table.clone())));
1176 }
1177
1178 // #433 — string filters.
1179 // df.filter_eq_str :: Table, Str, Str -> Result[Table, Str]
1180 // df.filter_in_str :: Table, Str, List[Str] -> Result[Table, Str]
1181 fields.insert("filter_eq_str".into(), Ty::function(
1182 vec![table.clone(), str_t.clone(), str_t.clone()],
1183 no_eff.clone(), res(table.clone())));
1184 fields.insert("filter_in_str".into(), Ty::function(
1185 vec![table.clone(), str_t.clone(), Ty::List(Box::new(str_t.clone()))],
1186 no_eff.clone(), res(table.clone())));
1187
1188 // #433 — float filters.
1189 // df.filter_{eq,lt,gt}_float :: Table, Str, Float -> Result[Table, Str]
1190 for name in &["filter_eq_float", "filter_lt_float", "filter_gt_float"] {
1191 fields.insert((*name).into(), Ty::function(
1192 vec![table.clone(), str_t.clone(), float_t.clone()],
1193 no_eff.clone(), res(table.clone())));
1194 }
1195
1196 // #433 — null handling.
1197 // df.filter_isnull :: Table, Str -> Result[Table, Str]
1198 // df.filter_notnull :: Table, Str -> Result[Table, Str]
1199 // df.drop_nulls :: Table, List[Str] -> Result[Table, Str]
1200 for name in &["filter_isnull", "filter_notnull"] {
1201 fields.insert((*name).into(), Ty::function(
1202 vec![table.clone(), str_t.clone()],
1203 no_eff.clone(), res(table.clone())));
1204 }
1205 fields.insert("drop_nulls".into(), Ty::function(
1206 vec![table.clone(), Ty::List(Box::new(str_t.clone()))],
1207 no_eff.clone(), res(table.clone())));
1208
1209 // df.sort_by :: Table, Str, Bool -> Result[Table, Str]
1210 fields.insert("sort_by".into(), Ty::function(
1211 vec![table.clone(), str_t.clone(), bool_t.clone()],
1212 no_eff.clone(), res(table.clone())));
1213
1214 // df.group_by_agg :: Table, List[Str], List[(Str, Str, Str)]
1215 // -> Result[Table, Str]
1216 // Spec tuple is (out_col, in_col, op). op ∈
1217 // "sum"|"mean"|"min"|"max"|"count"|"n_distinct".
1218 fields.insert("group_by_agg".into(), Ty::function(
1219 vec![
1220 table.clone(),
1221 Ty::List(Box::new(str_t.clone())),
1222 Ty::List(Box::new(Ty::Tuple(vec![
1223 str_t.clone(), str_t.clone(), str_t.clone(),
1224 ]))),
1225 ],
1226 no_eff.clone(), res(table.clone())));
1227
1228 // df.inner_join / left_join :: Table, Table, Str -> Result[Table, Str]
1229 for name in &["inner_join", "left_join"] {
1230 fields.insert((*name).into(), Ty::function(
1231 vec![table.clone(), table.clone(), str_t.clone()],
1232 no_eff.clone(), res(table.clone())));
1233 }
1234 // df.cross_join :: Table, Table -> Result[Table, Str]
1235 // Cartesian product; no join key. Clashing column names get
1236 // Polars' default `_right` suffix on the right side, same
1237 // as inner_join/left_join.
1238 fields.insert("cross_join".into(), Ty::function(
1239 vec![table.clone(), table.clone()],
1240 no_eff.clone(), res(table.clone())));
1241
1242 Some(Ty::Record(fields))
1243 }
1244 // `std.proc` was removed in favour of `std.process` (#678): its
1245 // single op `proc.spawn(cmd, args)` was byte-for-byte equivalent to
1246 // `process.run(cmd, args)` — same `[proc]` effect, same
1247 // `{ stdout, stderr, exit_code }` result — and `std.process` is a
1248 // strict superset (streaming spawn / read / wait / kill). Callers
1249 // migrate `proc.spawn` → `process.run`.
1250 "json" => {
1251 let mut fields = IndexMap::new();
1252 // stringify :: T -> Str (polymorphic on input)
1253 fields.insert("stringify".into(), Ty::function(
1254 vec![Ty::Var(0)], EffectSet::empty(), Ty::str(),
1255 ));
1256 // parse :: Str -> Result[T, Str]
1257 fields.insert("parse".into(), Ty::function(
1258 vec![Ty::str()], EffectSet::empty(),
1259 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1260 ));
1261 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
1262 // Tactical fix for #168 — caller passes the field names
1263 // T requires; runtime returns Err if any are missing
1264 // from the parsed object instead of letting field
1265 // access panic later.
1266 fields.insert("parse_strict".into(), Ty::function(
1267 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
1268 EffectSet::empty(),
1269 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
1270 ));
1271 // decode :: Str -> Result[Json, Str]
1272 // Parse into the generic `Json` value ADT (JNull/JBool/…),
1273 // total: the result is always a well-typed `Json`, so callers
1274 // walk it with pattern matches instead of trusting a shape.
1275 // Native (serde_json), O(n) — the drop-in for lex-schema's
1276 // interpreted `json_value.parse`. Distinct from `parse`
1277 // (which decodes into an inferred concrete type `T` and is
1278 // the subject of the parse_strict rewrite); `decode`'s
1279 // concrete `Json` return is never rewritten.
1280 let json_v = Ty::Con("Json".into(), vec![]);
1281 fields.insert("decode".into(), Ty::function(
1282 vec![Ty::str()], EffectSet::empty(),
1283 Ty::Con("Result".into(), vec![json_v.clone(), Ty::str()]),
1284 ));
1285 // encode :: Json -> Str (compact)
1286 fields.insert("encode".into(), Ty::function(
1287 vec![json_v.clone()], EffectSet::empty(), Ty::str(),
1288 ));
1289 // encode_pretty :: (Json, Int) -> Str (indent spaces per level)
1290 fields.insert("encode_pretty".into(), Ty::function(
1291 vec![json_v, Ty::int()], EffectSet::empty(), Ty::str(),
1292 ));
1293 Some(Ty::Record(fields))
1294 }
1295 "result" => {
1296 let mut fields = IndexMap::new();
1297 // result.map :: Result[T, E], (T) -> [E2] U -> [E2] Result[U, E]
1298 // Effect-polymorphic on the closure: result.map et al.
1299 // propagate the closure's effects to the surrounding call.
1300 fields.insert("map".into(), Ty::function(
1301 vec![
1302 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1303 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), Ty::Var(2)),
1304 ],
1305 EffectSet::open_var(3),
1306 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1307 ));
1308 fields.insert("and_then".into(), Ty::function(
1309 vec![
1310 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1311 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(4),
1312 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)])),
1313 ],
1314 EffectSet::open_var(4),
1315 Ty::Con("Result".into(), vec![Ty::Var(2), Ty::Var(1)]),
1316 ));
1317 fields.insert("map_err".into(), Ty::function(
1318 vec![
1319 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1320 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(5), Ty::Var(2)),
1321 ],
1322 EffectSet::open_var(5),
1323 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1324 ));
1325 // result.or_else :: Result[T, E1], (E1) -> [E] Result[T, E2]
1326 // -> [E] Result[T, E2]
1327 // Recovery combinator: closure runs only on Err and returns
1328 // the next Result (which itself may swap the error type).
1329 fields.insert("or_else".into(), Ty::function(
1330 vec![
1331 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1332 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(6),
1333 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)])),
1334 ],
1335 EffectSet::open_var(6),
1336 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(2)]),
1337 ));
1338 // result.unwrap_or :: Result[T, E], T -> T
1339 // Eager fallback — the Ok payload, or the supplied default on
1340 // Err. Mirrors option.unwrap_or (#679).
1341 fields.insert("unwrap_or".into(), Ty::function(
1342 vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]), Ty::Var(0)],
1343 EffectSet::empty(),
1344 Ty::Var(0),
1345 ));
1346 // result.unwrap_or_else :: Result[T, E], (E) -> [Eff] T -> [Eff] T
1347 // Lazy fallback — the closure runs only on Err and receives the
1348 // error payload (effect-polymorphic on the closure). Mirrors
1349 // option.unwrap_or_else (#679).
1350 fields.insert("unwrap_or_else".into(), Ty::function(
1351 vec![
1352 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1353 Ty::function(vec![Ty::Var(1)], EffectSet::open_var(7), Ty::Var(0)),
1354 ],
1355 EffectSet::open_var(7),
1356 Ty::Var(0),
1357 ));
1358 // result.is_ok / is_err :: Result[T, E] -> Bool (#679)
1359 fields.insert("is_ok".into(), Ty::function(
1360 vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1361 EffectSet::empty(), Ty::bool()));
1362 fields.insert("is_err".into(), Ty::function(
1363 vec![Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)])],
1364 EffectSet::empty(), Ty::bool()));
1365 Some(Ty::Record(fields))
1366 }
1367 "option" => {
1368 let mut fields = IndexMap::new();
1369 // option.map :: Option[T], (T) -> [E] U -> [E] Option[U]
1370 fields.insert("map".into(), Ty::function(
1371 vec![
1372 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1373 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1374 ],
1375 EffectSet::open_var(2),
1376 Ty::Con("Option".into(), vec![Ty::Var(1)]),
1377 ));
1378 // option.and_then :: Option[T], (T) -> [E] Option[U] -> [E] Option[U]
1379 // The compiler entry has been wired since the result/option
1380 // variant_map work landed; this signature was missed,
1381 // making the call fail to type-check until now.
1382 fields.insert("and_then".into(), Ty::function(
1383 vec![
1384 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1385 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
1386 Ty::Con("Option".into(), vec![Ty::Var(1)])),
1387 ],
1388 EffectSet::open_var(3),
1389 Ty::Con("Option".into(), vec![Ty::Var(1)]),
1390 ));
1391 fields.insert("unwrap_or".into(), Ty::function(
1392 vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(0)],
1393 EffectSet::empty(),
1394 Ty::Var(0),
1395 ));
1396 // option.unwrap_or_else :: Option[T], () -> [E] T -> [E] T
1397 // Lazy variant of unwrap_or: the default is computed by a closure
1398 // only when the value is None (effect-polymorphic on the closure).
1399 fields.insert("unwrap_or_else".into(), Ty::function(
1400 vec![
1401 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1402 Ty::function(vec![], EffectSet::open_var(5), Ty::Var(0)),
1403 ],
1404 EffectSet::open_var(5),
1405 Ty::Var(0),
1406 ));
1407 // option.or_else :: Option[T], () -> [E] Option[T] -> [E] Option[T]
1408 // The closure takes no arguments because None has no payload to pass.
1409 fields.insert("or_else".into(), Ty::function(
1410 vec![
1411 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1412 Ty::function(vec![], EffectSet::open_var(4),
1413 Ty::Con("Option".into(), vec![Ty::Var(0)])),
1414 ],
1415 EffectSet::open_var(4),
1416 Ty::Con("Option".into(), vec![Ty::Var(0)]),
1417 ));
1418 // option.is_some / is_none :: Option[T] -> Bool (#679)
1419 fields.insert("is_some".into(), Ty::function(
1420 vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1421 EffectSet::empty(), Ty::bool()));
1422 fields.insert("is_none".into(), Ty::function(
1423 vec![Ty::Con("Option".into(), vec![Ty::Var(0)])],
1424 EffectSet::empty(), Ty::bool()));
1425 // option.ok_or :: Option[T], E -> Result[T, E]
1426 // Cross from Option into Result, supplying the error for None (#679).
1427 fields.insert("ok_or".into(), Ty::function(
1428 vec![Ty::Con("Option".into(), vec![Ty::Var(0)]), Ty::Var(1)],
1429 EffectSet::empty(),
1430 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::Var(1)]),
1431 ));
1432 Some(Ty::Record(fields))
1433 }
1434 "tuple" => {
1435 // Tuple accessors per §11.1. Polymorphic in the tuple's
1436 // element types; we use the same row-variable shape used
1437 // by list helpers. Tuples are heterogeneous, so each
1438 // accessor is statically typed via independent type
1439 // variables for each position.
1440 let mut fields = IndexMap::new();
1441 // fst :: (T0, T1) -> T0
1442 fields.insert("fst".into(), Ty::function(
1443 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1444 EffectSet::empty(),
1445 Ty::Var(0),
1446 ));
1447 // snd :: (T0, T1) -> T1
1448 fields.insert("snd".into(), Ty::function(
1449 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1450 EffectSet::empty(),
1451 Ty::Var(1),
1452 ));
1453 // third :: (T0, T1, T2) -> T2
1454 fields.insert("third".into(), Ty::function(
1455 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1), Ty::Var(2)])],
1456 EffectSet::empty(),
1457 Ty::Var(2),
1458 ));
1459 // len :: (T0, T1) -> Int (covers any pair shape; Int back)
1460 fields.insert("len".into(), Ty::function(
1461 vec![Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])],
1462 EffectSet::empty(),
1463 Ty::int(),
1464 ));
1465 Some(Ty::Record(fields))
1466 }
1467 "map" => {
1468 // Persistent map. Keys are `Str` or `Int` only — Lex's
1469 // type system tracks them polymorphically as Var(0)
1470 // ("K") and lets the runtime check the key shape; both
1471 // cases fit into `MapKey`.
1472 //
1473 // Type variables: 0 = K, 1 = V.
1474 let mt = || Ty::Con("Map".into(), vec![Ty::Var(0), Ty::Var(1)]);
1475 let pair = || Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]);
1476 let mut fields = IndexMap::new();
1477 // new :: () -> Map[K, V]
1478 fields.insert("new".into(), Ty::function(
1479 vec![], EffectSet::empty(), mt()));
1480 // size :: Map[K, V] -> Int
1481 fields.insert("size".into(), Ty::function(
1482 vec![mt()], EffectSet::empty(), Ty::int()));
1483 // has :: Map[K, V], K -> Bool
1484 fields.insert("has".into(), Ty::function(
1485 vec![mt(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1486 // get :: Map[K, V], K -> Option[V]
1487 fields.insert("get".into(), Ty::function(
1488 vec![mt(), Ty::Var(0)], EffectSet::empty(),
1489 Ty::Con("Option".into(), vec![Ty::Var(1)])));
1490 // set :: Map[K, V], K, V -> Map[K, V]
1491 fields.insert("set".into(), Ty::function(
1492 vec![mt(), Ty::Var(0), Ty::Var(1)],
1493 EffectSet::empty(), mt()));
1494 // delete :: Map[K, V], K -> Map[K, V]
1495 fields.insert("delete".into(), Ty::function(
1496 vec![mt(), Ty::Var(0)], EffectSet::empty(), mt()));
1497 // keys :: Map[K, V] -> List[K]
1498 fields.insert("keys".into(), Ty::function(
1499 vec![mt()], EffectSet::empty(),
1500 Ty::List(Box::new(Ty::Var(0)))));
1501 // values :: Map[K, V] -> List[V]
1502 fields.insert("values".into(), Ty::function(
1503 vec![mt()], EffectSet::empty(),
1504 Ty::List(Box::new(Ty::Var(1)))));
1505 // entries :: Map[K, V] -> List[(K, V)]
1506 fields.insert("entries".into(), Ty::function(
1507 vec![mt()], EffectSet::empty(),
1508 Ty::List(Box::new(pair()))));
1509 // from_list :: List[(K, V)] -> Map[K, V]
1510 fields.insert("from_list".into(), Ty::function(
1511 vec![Ty::List(Box::new(pair()))],
1512 EffectSet::empty(), mt()));
1513 // merge :: Map[K, V], Map[K, V] -> Map[K, V] (b overrides a)
1514 fields.insert("merge".into(), Ty::function(
1515 vec![mt(), mt()], EffectSet::empty(), mt()));
1516 // is_empty :: Map[K, V] -> Bool
1517 fields.insert("is_empty".into(), Ty::function(
1518 vec![mt()], EffectSet::empty(), Ty::bool()));
1519 // fold :: Map[K, V], A, (A, K, V) -> [E] A -> [E] A
1520 // Iteration order matches `map.entries` (BTreeMap-sorted by
1521 // key). Effect-polymorphic on the combiner like `list.fold`.
1522 // Type variable 2 = A (accumulator), effect row 3.
1523 fields.insert("fold".into(), Ty::function(
1524 vec![
1525 mt(),
1526 Ty::Var(2),
1527 Ty::function(
1528 vec![Ty::Var(2), Ty::Var(0), Ty::Var(1)],
1529 EffectSet::open_var(3),
1530 Ty::Var(2),
1531 ),
1532 ],
1533 EffectSet::open_var(3),
1534 Ty::Var(2),
1535 ));
1536 Some(Ty::Record(fields))
1537 }
1538 "set" => {
1539 // Persistent set with the same key-type discipline as map.
1540 // Type variable: 0 = T (the element type, also the key type).
1541 let st = || Ty::Con("Set".into(), vec![Ty::Var(0)]);
1542 let mut fields = IndexMap::new();
1543 // new :: () -> Set[T]
1544 fields.insert("new".into(), Ty::function(
1545 vec![], EffectSet::empty(), st()));
1546 // size :: Set[T] -> Int
1547 fields.insert("size".into(), Ty::function(
1548 vec![st()], EffectSet::empty(), Ty::int()));
1549 // has :: Set[T], T -> Bool
1550 fields.insert("has".into(), Ty::function(
1551 vec![st(), Ty::Var(0)], EffectSet::empty(), Ty::bool()));
1552 // add :: Set[T], T -> Set[T]
1553 fields.insert("add".into(), Ty::function(
1554 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1555 // delete :: Set[T], T -> Set[T]
1556 fields.insert("delete".into(), Ty::function(
1557 vec![st(), Ty::Var(0)], EffectSet::empty(), st()));
1558 // to_list :: Set[T] -> List[T]
1559 fields.insert("to_list".into(), Ty::function(
1560 vec![st()], EffectSet::empty(),
1561 Ty::List(Box::new(Ty::Var(0)))));
1562 // from_list :: List[T] -> Set[T]
1563 fields.insert("from_list".into(), Ty::function(
1564 vec![Ty::List(Box::new(Ty::Var(0)))],
1565 EffectSet::empty(), st()));
1566 // union :: Set[T], Set[T] -> Set[T]
1567 fields.insert("union".into(), Ty::function(
1568 vec![st(), st()], EffectSet::empty(), st()));
1569 // intersect :: Set[T], Set[T] -> Set[T]
1570 fields.insert("intersect".into(), Ty::function(
1571 vec![st(), st()], EffectSet::empty(), st()));
1572 // diff :: Set[T], Set[T] -> Set[T]
1573 fields.insert("diff".into(), Ty::function(
1574 vec![st(), st()], EffectSet::empty(), st()));
1575 // is_empty :: Set[T] -> Bool
1576 fields.insert("is_empty".into(), Ty::function(
1577 vec![st()], EffectSet::empty(), Ty::bool()));
1578 // is_subset :: Set[T], Set[T] -> Bool (a is subset of b)
1579 fields.insert("is_subset".into(), Ty::function(
1580 vec![st(), st()], EffectSet::empty(), Ty::bool()));
1581 Some(Ty::Record(fields))
1582 }
1583 "iter" => {
1584 // Positional iterator (#364) + lazy variant via `iter.unfold`
1585 // (#376). Internal value shapes are `__IterEager(list, idx)` or
1586 // `__IterLazy(seed, step)`; all operations compile-inline and
1587 // dispatch on the variant tag at runtime.
1588 // Type var slots: 0 = T (element), 1 = U (mapped element) /
1589 // A (fold acc), 2 = S (unfold seed).
1590 let it = |n: u32| Ty::Con("Iter".into(), vec![Ty::Var(n)]);
1591 let mut fields = IndexMap::new();
1592 // from_list :: List[T] -> Iter[T]
1593 fields.insert("from_list".into(), Ty::function(
1594 vec![Ty::List(Box::new(Ty::Var(0)))],
1595 EffectSet::empty(), it(0)));
1596 // unfold[S, T] :: S, (S) -> Option[(T, S)] -> Iter[T] (#376)
1597 // The step closure may carry any effect row; the iterator
1598 // itself stays effect-free since the effects only fire when
1599 // the step is invoked via `iter.next` / `iter.to_list`.
1600 fields.insert("unfold".into(), Ty::function(
1601 vec![
1602 Ty::Var(2), // seed S
1603 Ty::function(
1604 vec![Ty::Var(2)],
1605 EffectSet::open_var(3),
1606 Ty::Con("Option".into(), vec![
1607 Ty::Tuple(vec![Ty::Var(0), Ty::Var(2)])
1608 ]),
1609 ),
1610 ],
1611 EffectSet::empty(), it(0)));
1612 // next :: Iter[T] -> Option[(T, Iter[T])]
1613 fields.insert("next".into(), Ty::function(
1614 vec![it(0)],
1615 EffectSet::empty(),
1616 Ty::Con("Option".into(), vec![
1617 Ty::Tuple(vec![Ty::Var(0), it(0)])
1618 ])));
1619 // is_empty :: Iter[T] -> Bool
1620 fields.insert("is_empty".into(), Ty::function(
1621 vec![it(0)], EffectSet::empty(), Ty::bool()));
1622 // count :: Iter[T] -> Int (remaining elements)
1623 fields.insert("count".into(), Ty::function(
1624 vec![it(0)], EffectSet::empty(), Ty::int()));
1625 // take :: Iter[T], Int -> Iter[T]
1626 fields.insert("take".into(), Ty::function(
1627 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1628 // skip :: Iter[T], Int -> Iter[T]
1629 fields.insert("skip".into(), Ty::function(
1630 vec![it(0), Ty::int()], EffectSet::empty(), it(0)));
1631 // to_list :: Iter[T] -> List[T]
1632 fields.insert("to_list".into(), Ty::function(
1633 vec![it(0)], EffectSet::empty(),
1634 Ty::List(Box::new(Ty::Var(0)))));
1635 // collect :: Iter[T] -> List[T] — alias for `to_list`
1636 // (matches Rust / Python / Kotlin naming so call sites
1637 // coming from those languages don't have to re-learn).
1638 fields.insert("collect".into(), Ty::function(
1639 vec![it(0)], EffectSet::empty(),
1640 Ty::List(Box::new(Ty::Var(0)))));
1641 // map :: [E] Iter[T], (T) -> [E] U -> [E] Iter[U]
1642 fields.insert("map".into(), Ty::function(
1643 vec![
1644 it(0),
1645 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1646 ],
1647 EffectSet::open_var(2), it(1)));
1648 // filter :: [E] Iter[T], (T) -> [E] Bool -> [E] Iter[T]
1649 fields.insert("filter".into(), Ty::function(
1650 vec![
1651 it(0),
1652 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(1), Ty::bool()),
1653 ],
1654 EffectSet::open_var(1), it(0)));
1655 // fold :: [E] Iter[T], A, (A, T) -> [E] A -> [E] A
1656 fields.insert("fold".into(), Ty::function(
1657 vec![
1658 it(0),
1659 Ty::Var(1),
1660 Ty::function(vec![Ty::Var(1), Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
1661 ],
1662 EffectSet::open_var(2), Ty::Var(1)));
1663 Some(Ty::Record(fields))
1664 }
1665 "flow" => {
1666 // Orchestration primitives (spec §11.2). Each takes one or
1667 // more closures and returns a closure with a derived shape.
1668 let mut fields = IndexMap::new();
1669 // sequential[T, U, V](f: (T) -> U, g: (U) -> V) -> (T) -> V
1670 fields.insert("sequential".into(), Ty::function(
1671 vec![
1672 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1673 Ty::function(vec![Ty::Var(1)], EffectSet::empty(), Ty::Var(2)),
1674 ],
1675 EffectSet::empty(),
1676 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(2)),
1677 ));
1678 // branch[T, U](cond: (T) -> Bool, t: (T) -> U, f: (T) -> U) -> (T) -> U
1679 fields.insert("branch".into(), Ty::function(
1680 vec![
1681 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::bool()),
1682 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1683 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1684 ],
1685 EffectSet::empty(),
1686 Ty::function(vec![Ty::Var(0)], EffectSet::empty(), Ty::Var(1)),
1687 ));
1688 // retry[T, U, E, Eff](
1689 // f: (T) -> [Eff] Result[U, E], n: Int
1690 // ) -> (T) -> [Eff] Result[U, E]
1691 // open_var(3) is the effect row carried by `f`; the
1692 // combinator itself is pure, so the outer EffectSet is
1693 // empty. The returned closure propagates Eff unchanged.
1694 let result_ty = Ty::Con("Result".into(), vec![Ty::Var(1), Ty::Var(2)]);
1695 fields.insert("retry".into(), Ty::function(
1696 vec![
1697 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1698 Ty::int(),
1699 ],
1700 EffectSet::empty(),
1701 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1702 ));
1703 // retry_with_backoff[T, U, E, Eff](
1704 // f: (T) -> [Eff] Result[U, E], attempts: Int, base_ms: Int,
1705 // ) -> (T) -> [Eff, time] Result[U, E]
1706 // Same retry shape as `flow.retry` plus an exponential
1707 // backoff between attempts. The result function carries
1708 // `[time]` (from `time.sleep_ms`) unioned with the inner
1709 // closure's effect row Eff, so e.g. a `[net]` closure
1710 // produces a `[net, time]` result function. (#226)
1711 fields.insert("retry_with_backoff".into(), Ty::function(
1712 vec![
1713 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3), result_ty.clone()),
1714 Ty::int(),
1715 Ty::int(),
1716 ],
1717 EffectSet::empty(),
1718 Ty::function(vec![Ty::Var(0)],
1719 EffectSet::open_var(3).union(&EffectSet::singleton("time")), result_ty),
1720 ));
1721 // parallel[A, B](fa: () -> A, fb: () -> B) -> () -> (A, B)
1722 // Sequential implementation today; spec §11.2 reserves the
1723 // option of a true-threaded scheduler. parallel_record is
1724 // listed in the spec but not yet implemented — it needs row
1725 // polymorphism over the input record's fields plus a
1726 // record-iteration trampoline; tracked as follow-up.
1727 fields.insert("parallel".into(), Ty::function(
1728 vec![
1729 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1730 Ty::function(vec![], EffectSet::empty(), Ty::Var(1)),
1731 ],
1732 EffectSet::empty(),
1733 Ty::function(vec![], EffectSet::empty(),
1734 Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)])),
1735 ));
1736 // parallel_list[T](actions: List[() -> T]) -> List[T]
1737 // Variadic counterpart to `parallel`. Runs each action and
1738 // collects results in input order. Sequential under the
1739 // hood (same caveat as `parallel`); spec §11.2 reserves
1740 // true threading for a future scheduler. Unlike `parallel`,
1741 // this returns the result list directly rather than a
1742 // closure, since the input arity is dynamic.
1743 fields.insert("parallel_list".into(), Ty::function(
1744 vec![
1745 Ty::List(Box::new(
1746 Ty::function(vec![], EffectSet::empty(), Ty::Var(0)),
1747 )),
1748 ],
1749 EffectSet::empty(),
1750 Ty::List(Box::new(Ty::Var(0))),
1751 ));
1752 Some(Ty::Record(fields))
1753 }
1754 "crypto" => {
1755 let mut fields = IndexMap::new();
1756 // Hashes: Bytes -> Bytes (digest as raw bytes).
1757 // SHA-256 / SHA-512 are vetted. MD5 is retained only for
1758 // interop with legacy systems — new code should not use it.
1759 // BLAKE2b (#382) is included as a faster alternative to
1760 // SHA-512 with the same security level.
1761 for name in &["sha256", "sha512", "md5", "blake2b"] {
1762 fields.insert((*name).into(), Ty::function(
1763 vec![Ty::bytes()],
1764 EffectSet::empty(),
1765 Ty::bytes(),
1766 ));
1767 }
1768 // Hex-string convenience hashers (#382): hash a Str directly,
1769 // return the digest as a lowercase hex Str. Equivalent to
1770 // `crypto.hex_encode(crypto.shaN(bytes_from_str(s)))` but
1771 // saves the two-step incantation for the common case.
1772 for name in &["sha256_str", "sha512_str"] {
1773 fields.insert((*name).into(), Ty::function(
1774 vec![Ty::str()],
1775 EffectSet::empty(),
1776 Ty::str(),
1777 ));
1778 }
1779 // HMAC: (key :: Bytes, data :: Bytes) -> Bytes
1780 for name in &["hmac_sha256", "hmac_sha512"] {
1781 fields.insert((*name).into(), Ty::function(
1782 vec![Ty::bytes(), Ty::bytes()],
1783 EffectSet::empty(),
1784 Ty::bytes(),
1785 ));
1786 }
1787 // ed25519 asymmetric signatures (#643). A secret key is its 32-byte
1788 // seed (generate via `crypto.random(32)`); all three ops are pure.
1789 // ed25519_public_key(secret :: Bytes) -> Result[Bytes, Str]
1790 // ed25519_sign(secret :: Bytes, message :: Bytes) -> Result[Bytes, Str]
1791 // ed25519_verify(public :: Bytes, message :: Bytes, sig :: Bytes) -> Bool
1792 fields.insert("ed25519_public_key".into(), Ty::function(
1793 vec![Ty::bytes()],
1794 EffectSet::empty(),
1795 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1796 ));
1797 fields.insert("ed25519_sign".into(), Ty::function(
1798 vec![Ty::bytes(), Ty::bytes()],
1799 EffectSet::empty(),
1800 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1801 ));
1802 fields.insert("ed25519_verify".into(), Ty::function(
1803 vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1804 EffectSet::empty(),
1805 Ty::bool(),
1806 ));
1807 // Whether `bytes` decompresses to a valid point on the Edwards25519
1808 // curve (#93 follow-up: Solana Program Derived Address search --
1809 // a PDA is valid iff the candidate 32 bytes are NOT a valid point,
1810 // so callers try bump seeds until this returns false).
1811 // ed25519_is_valid_point(bytes :: Bytes) -> Bool
1812 fields.insert("ed25519_is_valid_point".into(), Ty::function(
1813 vec![Ty::bytes()],
1814 EffectSet::empty(),
1815 Ty::bool(),
1816 ));
1817 // P-256 ECDSA / ES256 (#651). The JWT/SD-JWT signature
1818 // algorithm for AP2 agent keys; `lex-jose` builds the
1819 // token layer on top of these primitives. Key bytes are
1820 // raw: a secret key is the 32-byte scalar, a public key is
1821 // the 33-byte SEC1 compressed point; JWK serialization
1822 // lives downstream in `lex-jose`.
1823 //
1824 // p256_generate() -> [random] Result[Bytes, Str]
1825 // p256_public_key(sk :: Bytes) -> Result[Bytes, Str]
1826 // p256_sign(sk :: Bytes, msg :: Bytes) -> Result[Bytes, Str]
1827 // p256_verify(pk :: Bytes, msg :: Bytes, sig :: Bytes) -> Bool
1828 //
1829 // `p256_generate` mints fresh key material from the OS RNG,
1830 // so it carries the same fine-grained `[random]` effect as
1831 // `crypto.random` — every key-minting call stays visible to
1832 // `lex audit --effect random`. (The issue sketched `[env]`;
1833 // `[random]` is the dedicated effect for OS randomness in
1834 // this codebase, so we use that for consistency.)
1835 // `sign`/`verify` are pure: signing hashes `msg` with
1836 // SHA-256 internally (standard ES256) and the signature is
1837 // DER-encoded.
1838 fields.insert("p256_generate".into(), Ty::function(
1839 vec![],
1840 EffectSet::singleton("random"),
1841 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1842 ));
1843 fields.insert("p256_public_key".into(), Ty::function(
1844 vec![Ty::bytes()],
1845 EffectSet::empty(),
1846 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1847 ));
1848 fields.insert("p256_sign".into(), Ty::function(
1849 vec![Ty::bytes(), Ty::bytes()],
1850 EffectSet::empty(),
1851 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1852 ));
1853 fields.insert("p256_verify".into(), Ty::function(
1854 vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1855 EffectSet::empty(),
1856 Ty::bool(),
1857 ));
1858 // secp256k1 ECDSA + recovery (#655). The EVM curve — backs
1859 // EIP-712 typed-data signing (EIP-3009 / x402 `exact`) and
1860 // Ethereum address derivation. Unlike `p256_*`/`ed25519_*`,
1861 // the sign/verify ops here take a **pre-hashed 32-byte
1862 // digest** (EIP-712 already hashes), hence the `_digest`
1863 // suffix — they do NOT hash the input again.
1864 //
1865 // - Secret key: 32-byte scalar.
1866 // - Public key: 65-byte UNCOMPRESSED SEC1 point (0x04‖X‖Y),
1867 // so an address is `keccak256(pk[1..])[12..]` with no
1868 // decompression step. (p256 returns compressed; the EVM
1869 // convention is uncompressed.)
1870 // - Signature: 65 bytes `r(32)‖s(32)‖v(1)`, v ∈ {27,28}
1871 // (Ethereum), low-S normalized (EIP-2).
1872 //
1873 // keccak256(data :: Bytes) -> Bytes
1874 // secp256k1_generate() -> [random] Result[Bytes, Str]
1875 // secp256k1_public_key(sk :: Bytes) -> Result[Bytes, Str]
1876 // secp256k1_sign_digest(sk :: Bytes, digest :: Bytes) -> Result[Bytes, Str]
1877 // secp256k1_recover(digest :: Bytes, sig :: Bytes) -> Result[Bytes, Str]
1878 // secp256k1_verify(pk :: Bytes, digest :: Bytes, sig :: Bytes) -> Bool
1879 //
1880 // `secp256k1_generate` mints from the OS RNG, so it carries
1881 // the same `[random]` effect as `crypto.random` / `p256_generate`.
1882 fields.insert("keccak256".into(), Ty::function(
1883 vec![Ty::bytes()],
1884 EffectSet::empty(),
1885 Ty::bytes(),
1886 ));
1887 fields.insert("secp256k1_generate".into(), Ty::function(
1888 vec![],
1889 EffectSet::singleton("random"),
1890 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1891 ));
1892 fields.insert("secp256k1_public_key".into(), Ty::function(
1893 vec![Ty::bytes()],
1894 EffectSet::empty(),
1895 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1896 ));
1897 fields.insert("secp256k1_sign_digest".into(), Ty::function(
1898 vec![Ty::bytes(), Ty::bytes()],
1899 EffectSet::empty(),
1900 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1901 ));
1902 fields.insert("secp256k1_recover".into(), Ty::function(
1903 vec![Ty::bytes(), Ty::bytes()],
1904 EffectSet::empty(),
1905 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
1906 ));
1907 fields.insert("secp256k1_verify".into(), Ty::function(
1908 vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
1909 EffectSet::empty(),
1910 Ty::bool(),
1911 ));
1912 // base64 / hex
1913 fields.insert("base64_encode".into(), Ty::function(
1914 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1915 fields.insert("base64_decode".into(), Ty::function(
1916 vec![Ty::str()], EffectSet::empty(),
1917 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1918 // URL-safe base64 (#382): the alphabet swaps `+/` for `-_`
1919 // and omits padding. Required by JWT, signed-cookie, and
1920 // most token-bearing URL paths.
1921 fields.insert("base64url_encode".into(), Ty::function(
1922 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1923 fields.insert("base64url_decode".into(), Ty::function(
1924 vec![Ty::str()], EffectSet::empty(),
1925 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1926 fields.insert("hex_encode".into(), Ty::function(
1927 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1928 fields.insert("hex_decode".into(), Ty::function(
1929 vec![Ty::str()], EffectSet::empty(),
1930 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1931 // base58 (#658) — Bitcoin/Solana alphabet, no checksum. Solana
1932 // addresses, mints, signatures and the x402 `exact` payload are
1933 // base58; this is the Solana analog of keccak/secp256k1 (#655).
1934 fields.insert("base58_encode".into(), Ty::function(
1935 vec![Ty::bytes()], EffectSet::empty(), Ty::str()));
1936 fields.insert("base58_decode".into(), Ty::function(
1937 vec![Ty::str()], EffectSet::empty(),
1938 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()])));
1939 // Constant-time equality (for HMAC verification etc.).
1940 // `eq` / `eq_str` (#382) are the recommended spelling;
1941 // `constant_time_eq` stays as a deprecated alias.
1942 fields.insert("constant_time_eq".into(), Ty::function(
1943 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1944 fields.insert("eq".into(), Ty::function(
1945 vec![Ty::bytes(), Ty::bytes()], EffectSet::empty(), Ty::bool()));
1946 fields.insert("eq_str".into(), Ty::function(
1947 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
1948 // Cryptographically-secure random bytes — OS RNG, not the
1949 // deterministic `rand.int_in` stub. The new `[random]`
1950 // effect is fine-grained on purpose so reviewers can find
1951 // every token-generating call via `lex audit --effect
1952 // random`.
1953 fields.insert("random".into(), Ty::function(
1954 vec![Ty::int()],
1955 EffectSet::singleton("random"),
1956 Ty::bytes(),
1957 ));
1958 // random_str_hex (#382): the most common token-mint pattern
1959 // — N random bytes rendered as 2N lowercase hex chars.
1960 // Suitable for session ids, request ids, OAuth `state`,
1961 // CSRF tokens; not suitable as a JWT signing key (use raw
1962 // `random` for that).
1963 fields.insert("random_str_hex".into(), Ty::function(
1964 vec![Ty::int()],
1965 EffectSet::singleton("random"),
1966 Ty::str(),
1967 ));
1968
1969 // AEAD: authenticated encryption with associated data
1970 // (#382 AEAD slice). Both algorithms use a 12-byte nonce
1971 // and a 16-byte authentication tag. `seal` returns the
1972 // structured `AeadResult { ciphertext, tag }`; `open`
1973 // returns `Result[Bytes, Str]` so authentication failures
1974 // surface as `Err`, not a panic.
1975 //
1976 // - **AES-GCM** (`aes_gcm_seal/open`): AES-128/192/256-GCM,
1977 // key length determined by the supplied key bytes (16, 24,
1978 // or 32). NIST-recommended; hardware-accelerated on most CPUs.
1979 // - **ChaCha20-Poly1305** (`chacha20_poly1305_seal/open`):
1980 // Always a 32-byte key. Equivalent security to AES-GCM
1981 // without needing AES-NI hardware; preferred on constrained
1982 // targets.
1983 let aead_t = || Ty::Con("AeadResult".into(), vec![]);
1984 // Seal: returns Result[AeadResult, Str] rather than bare
1985 // AeadResult so input-validation errors (wrong key length,
1986 // wrong nonce length) surface as `Err` to the Lex caller
1987 // instead of panicking the VM. AES-GCM expects 16/24/32-byte
1988 // keys; ChaCha20-Poly1305 expects exactly 32. Both expect a
1989 // 12-byte nonce.
1990 for name in &["aes_gcm_seal", "chacha20_poly1305_seal"] {
1991 fields.insert((*name).into(), Ty::function(
1992 // (key, nonce, aad, plaintext) -> Result[AeadResult, Str]
1993 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
1994 EffectSet::empty(),
1995 Ty::Con("Result".into(), vec![aead_t(), Ty::str()]),
1996 ));
1997 }
1998 for name in &["aes_gcm_open", "chacha20_poly1305_open"] {
1999 fields.insert((*name).into(), Ty::function(
2000 // (key, nonce, aad, ciphertext, tag) -> Result[Bytes, Str]
2001 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::bytes()],
2002 EffectSet::empty(),
2003 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2004 ));
2005 }
2006
2007 // AES-CBC, UNAUTHENTICATED (#760). Retained on the same footing
2008 // as `md5` above: present because protocols someone else designed
2009 // demand it, never because it is a good way to protect anything.
2010 // New code wants `aes_gcm_seal`.
2011 //
2012 // The `_raw` suffix is deliberate and load-bearing. There is no
2013 // MAC here, so ciphertext is malleable and decryption is a
2014 // padding oracle for anyone who can feed it input. A name that
2015 // sat naturally beside `aes_gcm_seal` would eventually be reached
2016 // for by someone storing a secret, which is the failure this
2017 // naming exists to make awkward.
2018 //
2019 // aes_cbc_encrypt_raw(key, iv, plaintext) -> Result[Bytes, Str]
2020 // aes_cbc_decrypt_raw(key, iv, ciphertext) -> Result[Bytes, Str]
2021 //
2022 // Key length selects the variant (16/24/32 -> AES-128/192/256),
2023 // matching how `aes_gcm_seal` already infers from the key slice.
2024 // The IV is always 16 bytes. Padding is PKCS#7, applied on
2025 // encrypt and validated on decrypt — a bad pad is `Err`, not a
2026 // panic and not silently-truncated plaintext.
2027 for name in &["aes_cbc_encrypt_raw", "aes_cbc_decrypt_raw"] {
2028 fields.insert((*name).into(), Ty::function(
2029 vec![Ty::bytes(), Ty::bytes(), Ty::bytes()],
2030 EffectSet::empty(),
2031 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2032 ));
2033 }
2034
2035 // KDFs: key-derivation functions (#382 KDF slice). All three
2036 // return `Result[Bytes, Str]` so caller-controlled inputs
2037 // (iteration count, output length, argon2id work factors)
2038 // that violate the underlying primitive's contract surface
2039 // as `Err` rather than panicking the VM. None require a new
2040 // effect — these are pure derivations.
2041 //
2042 // - **`pbkdf2_sha256(password, salt, iterations, len)`** —
2043 // RFC 8018 PBKDF2 with HMAC-SHA256. Use ≥ 600_000 iterations
2044 // for password storage (OWASP 2024). Older deployments
2045 // pinning < 100_000 should rotate.
2046 // - **`hkdf_sha256(ikm, salt, info, len)`** — RFC 5869 extract+
2047 // expand. Use for deriving multiple keys from a single
2048 // high-entropy input (TLS, Noise, JWT-key rotation).
2049 // Output length capped at 255 × 32 = 8160 bytes.
2050 // - **`argon2id(password, salt, t_cost, m_cost, len)`** —
2051 // RFC 9106 Argon2id. Recommended for *new* password
2052 // hashing. OWASP 2024 baseline: `t_cost=2, m_cost=19456`
2053 // (19 MiB), or use `lex-crypto`'s vetted wrapper.
2054 fields.insert("pbkdf2_sha256".into(), Ty::function(
2055 // (password, salt, iterations, len) -> Result[Bytes, Str]
2056 vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int()],
2057 EffectSet::empty(),
2058 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2059 ));
2060 fields.insert("hkdf_sha256".into(), Ty::function(
2061 // (ikm, salt, info, len) -> Result[Bytes, Str]
2062 vec![Ty::bytes(), Ty::bytes(), Ty::bytes(), Ty::int()],
2063 EffectSet::empty(),
2064 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2065 ));
2066 fields.insert("argon2id".into(), Ty::function(
2067 // (password, salt, t_cost, m_cost, len) -> Result[Bytes, Str]
2068 vec![Ty::bytes(), Ty::bytes(), Ty::int(), Ty::int(), Ty::int()],
2069 EffectSet::empty(),
2070 Ty::Con("Result".into(), vec![Ty::bytes(), Ty::str()]),
2071 ));
2072
2073 Some(Ty::Record(fields))
2074 }
2075 "deque" => {
2076 // Persistent double-ended queue. Push/pop O(1) on both
2077 // ends; iteration order is front-to-back.
2078 // Type variable: 0 = T.
2079 let dt = || Ty::Con("Deque".into(), vec![Ty::Var(0)]);
2080 let pair = || Ty::Tuple(vec![Ty::Var(0), dt()]);
2081 let mut fields = IndexMap::new();
2082 // new :: () -> Deque[T]
2083 fields.insert("new".into(), Ty::function(
2084 vec![], EffectSet::empty(), dt()));
2085 // size :: Deque[T] -> Int
2086 fields.insert("size".into(), Ty::function(
2087 vec![dt()], EffectSet::empty(), Ty::int()));
2088 // is_empty :: Deque[T] -> Bool
2089 fields.insert("is_empty".into(), Ty::function(
2090 vec![dt()], EffectSet::empty(), Ty::bool()));
2091 // push_back / push_front :: Deque[T], T -> Deque[T]
2092 for n in &["push_back", "push_front"] {
2093 fields.insert((*n).into(), Ty::function(
2094 vec![dt(), Ty::Var(0)], EffectSet::empty(), dt()));
2095 }
2096 // pop_back / pop_front :: Deque[T] -> Option[(T, Deque[T])]
2097 for n in &["pop_back", "pop_front"] {
2098 fields.insert((*n).into(), Ty::function(
2099 vec![dt()], EffectSet::empty(),
2100 Ty::Con("Option".into(), vec![pair()])));
2101 }
2102 // peek_back / peek_front :: Deque[T] -> Option[T]
2103 for n in &["peek_back", "peek_front"] {
2104 fields.insert((*n).into(), Ty::function(
2105 vec![dt()], EffectSet::empty(),
2106 Ty::Con("Option".into(), vec![Ty::Var(0)])));
2107 }
2108 // from_list :: List[T] -> Deque[T]
2109 fields.insert("from_list".into(), Ty::function(
2110 vec![Ty::List(Box::new(Ty::Var(0)))],
2111 EffectSet::empty(), dt()));
2112 // to_list :: Deque[T] -> List[T]
2113 fields.insert("to_list".into(), Ty::function(
2114 vec![dt()], EffectSet::empty(),
2115 Ty::List(Box::new(Ty::Var(0)))));
2116 Some(Ty::Record(fields))
2117 }
2118 "log" => {
2119 // Structured logging behind a [log] effect. Emit ops route
2120 // through a runtime-configured sink (stderr by default;
2121 // can be redirected via set_sink). Configuration ops
2122 // mutate the global sink and so are gated [io].
2123 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2124 let mut fields = IndexMap::new();
2125 for level in &["debug", "info", "warn", "error"] {
2126 fields.insert((*level).into(), Ty::function(
2127 vec![Ty::str()],
2128 EffectSet::singleton("log"),
2129 Ty::Unit,
2130 ));
2131 }
2132 // set_level :: Str -> [io] Result[Unit, Str]
2133 fields.insert("set_level".into(), Ty::function(
2134 vec![Ty::str()],
2135 EffectSet::singleton("io"),
2136 result_str(Ty::Unit)));
2137 // set_format :: Str -> [io] Result[Unit, Str]
2138 fields.insert("set_format".into(), Ty::function(
2139 vec![Ty::str()],
2140 EffectSet::singleton("io"),
2141 result_str(Ty::Unit)));
2142 // set_sink :: Str -> [io, fs_write] Result[Unit, Str]
2143 fields.insert("set_sink".into(), Ty::function(
2144 vec![Ty::str()],
2145 EffectSet {
2146 concrete: [crate::types::EffectKind::bare("io"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2147 var: None,
2148 },
2149 result_str(Ty::Unit)));
2150 Some(Ty::Record(fields))
2151 }
2152 "datetime" => {
2153 // Instant and Duration are nominal opaque Ints under the
2154 // hood (nanoseconds-since-UTC-epoch and signed nanoseconds
2155 // respectively); the type checker tracks the distinction
2156 // even though both values look like Int at runtime.
2157 //
2158 // Tz is the variant
2159 // Utc | Local | Offset(Int) | Iana(Str)
2160 // registered as a built-in nominal type in
2161 // `TypeEnv::new_with_builtins`. The pre-v1 stringly Tz
2162 // ("UTC"/"Local"/IANA-name/"+05:30") is no longer accepted
2163 // — passing a `Str` to `to_components` is now a type
2164 // error.
2165 let inst = || Ty::Con("Instant".into(), vec![]);
2166 let dur = || Ty::Con("Duration".into(), vec![]);
2167 let tz = || Ty::Con("Tz".into(), vec![]);
2168 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2169 let dt_t = || {
2170 let mut fs = IndexMap::new();
2171 fs.insert("year".into(), Ty::int());
2172 fs.insert("month".into(), Ty::int());
2173 fs.insert("day".into(), Ty::int());
2174 fs.insert("hour".into(), Ty::int());
2175 fs.insert("minute".into(), Ty::int());
2176 fs.insert("second".into(), Ty::int());
2177 fs.insert("nano".into(), Ty::int());
2178 fs.insert("tz_offset_minutes".into(), Ty::int());
2179 Ty::Record(fs)
2180 };
2181 let mut fields = IndexMap::new();
2182 fields.insert("now".into(), Ty::function(
2183 vec![], EffectSet::singleton("time"), inst()));
2184 fields.insert("parse_iso".into(), Ty::function(
2185 vec![Ty::str()], EffectSet::empty(), result_str(inst())));
2186 fields.insert("format_iso".into(), Ty::function(
2187 vec![inst()], EffectSet::empty(), Ty::str()));
2188 fields.insert("parse".into(), Ty::function(
2189 vec![Ty::str(), Ty::str()], EffectSet::empty(), result_str(inst())));
2190 fields.insert("format".into(), Ty::function(
2191 vec![inst(), Ty::str()], EffectSet::empty(), Ty::str()));
2192 fields.insert("to_components".into(), Ty::function(
2193 vec![inst(), tz()], EffectSet::empty(), result_str(dt_t())));
2194 fields.insert("from_components".into(), Ty::function(
2195 vec![dt_t()], EffectSet::empty(), result_str(inst())));
2196 fields.insert("add".into(), Ty::function(
2197 vec![inst(), dur()], EffectSet::empty(), inst()));
2198 fields.insert("diff".into(), Ty::function(
2199 vec![inst(), inst()], EffectSet::empty(), dur()));
2200 fields.insert("duration_seconds".into(), Ty::function(
2201 vec![Ty::float()], EffectSet::empty(), dur()));
2202 fields.insert("duration_minutes".into(), Ty::function(
2203 vec![Ty::int()], EffectSet::empty(), dur()));
2204 fields.insert("duration_days".into(), Ty::function(
2205 vec![Ty::int()], EffectSet::empty(), dur()));
2206 // #331: comparison ops on Instant.
2207 fields.insert("before".into(), Ty::function(
2208 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2209 fields.insert("after".into(), Ty::function(
2210 vec![inst(), inst()], EffectSet::empty(), Ty::bool()));
2211 // compare :: Instant, Instant -> Int (-1 / 0 / +1)
2212 fields.insert("compare".into(), Ty::function(
2213 vec![inst(), inst()], EffectSet::empty(), Ty::int()));
2214 Some(Ty::Record(fields))
2215 }
2216 // #331: duration module — scalar extraction from Duration values.
2217 "duration" => {
2218 let dur = || Ty::Con("Duration".into(), vec![]);
2219 let mut fields = IndexMap::new();
2220 // Scalar extraction from a Duration (nanoseconds under the
2221 // hood). Each truncates toward zero. `seconds` shipped with
2222 // #331; #681 rounds out the unit set so a Duration built in
2223 // days via `datetime.duration_days` can be read back in the
2224 // same units rather than only as seconds.
2225 // millis / seconds / minutes / hours / days :: Duration -> Int
2226 for name in &["millis", "seconds", "minutes", "hours", "days"] {
2227 fields.insert((*name).into(), Ty::function(
2228 vec![dur()], EffectSet::empty(), Ty::int()));
2229 }
2230 Some(Ty::Record(fields))
2231 }
2232 "approval" => {
2233 // Human-in-the-loop host boundary. `request(scope, reason)`
2234 // blocks until an operator answers via the configured
2235 // `ApprovalSink` (default: reject — a handler must opt in
2236 // with `with_approval_sink`, or `lex run` must be given an
2237 // interactive one). `scope` selects the approval channel
2238 // ("payment", "deploy", ...) and is checked at call time
2239 // against `--allow-approval`, mirroring how `net`'s host
2240 // arg is checked against `--allow-net-host` rather than
2241 // being encoded in the declared effect type.
2242 let mut fields = IndexMap::new();
2243 // request :: Str, Str -> [approval] Result[Str, Str]
2244 fields.insert("request".into(), Ty::function(
2245 vec![Ty::str(), Ty::str()],
2246 EffectSet::singleton("approval"),
2247 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()])));
2248 Some(Ty::Record(fields))
2249 }
2250 "process" => {
2251 // Streaming subprocess. The opaque `ProcessHandle` type
2252 // is an Int handle into a process-wide registry holding
2253 // the `Child` plus its stdout/stderr `BufReader`s.
2254 let ph = || Ty::Con("ProcessHandle".into(), vec![]);
2255 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2256 let opts_t = || {
2257 let mut fs = IndexMap::new();
2258 fs.insert("cwd".into(),
2259 Ty::Con("Option".into(), vec![Ty::str()]));
2260 fs.insert("env".into(),
2261 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
2262 fs.insert("stdin".into(),
2263 Ty::Con("Option".into(), vec![Ty::bytes()]));
2264 Ty::Record(fs)
2265 };
2266 let exit_t = || {
2267 let mut fs = IndexMap::new();
2268 fs.insert("code".into(), Ty::int());
2269 fs.insert("signaled".into(), Ty::bool());
2270 Ty::Record(fs)
2271 };
2272 let output_t = || {
2273 let mut fs = IndexMap::new();
2274 fs.insert("stdout".into(), Ty::str());
2275 fs.insert("stderr".into(), Ty::str());
2276 fs.insert("exit_code".into(), Ty::int());
2277 Ty::Record(fs)
2278 };
2279 let mut fields = IndexMap::new();
2280 // spawn :: Str, List[Str], Opts -> [proc] Result[ProcessHandle, Str]
2281 fields.insert("spawn".into(), Ty::function(
2282 vec![Ty::str(), Ty::List(Box::new(Ty::str())), opts_t()],
2283 EffectSet::singleton("proc"),
2284 result_str(ph())));
2285 // read_stdout_line / read_stderr_line :: ProcessHandle -> [proc] Option[Str]
2286 for n in &["read_stdout_line", "read_stderr_line"] {
2287 fields.insert((*n).into(), Ty::function(
2288 vec![ph()], EffectSet::singleton("proc"),
2289 Ty::Con("Option".into(), vec![Ty::str()])));
2290 }
2291 // wait :: ProcessHandle -> [proc] ProcessExit
2292 fields.insert("wait".into(), Ty::function(
2293 vec![ph()], EffectSet::singleton("proc"), exit_t()));
2294 // kill :: ProcessHandle, Str -> [proc] Result[Unit, Str]
2295 fields.insert("kill".into(), Ty::function(
2296 vec![ph(), Ty::str()],
2297 EffectSet::singleton("proc"),
2298 result_str(Ty::Unit)));
2299 // exit :: Int -> [proc_exit] Unit
2300 //
2301 // Sets the status `lex run` terminates with, so a Lex
2302 // program can be called by a shell script for its verdict
2303 // (#754). Its own effect kind rather than `proc`: running a
2304 // subprocess and ending your caller's process are different
2305 // authorities, and a program allowed to shell out should not
2306 // thereby be allowed to decide what its invoker sees.
2307 //
2308 // The declared return is `Unit` because the language has no
2309 // bottom type; nothing after a successful `exit` runs. The
2310 // call unwinds the VM rather than terminating the process
2311 // where it stands, so `lex run` still finalises its trace
2312 // and writes its attestations before exiting — a program
2313 // that exits is still a run that happened.
2314 //
2315 // Only the *first* exit is honoured. A program that calls
2316 // exit twice has already stopped at the first.
2317 fields.insert("exit".into(), Ty::function(
2318 vec![Ty::int()],
2319 EffectSet::singleton("proc_exit"),
2320 Ty::Unit));
2321 // run :: Str, List[Str] -> [proc] Result[ProcessOutput, Str]
2322 // Blocking convenience that captures stdout/stderr fully
2323 // and returns once the child exits. For programs that
2324 // need streaming, use spawn + read_*_line + wait.
2325 fields.insert("run".into(), Ty::function(
2326 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
2327 EffectSet::singleton("proc"),
2328 result_str(output_t())));
2329 Some(Ty::Record(fields))
2330 }
2331 "fs" => {
2332 // Filesystem walk + mutate. Walk-style ops (exists, walk,
2333 // glob, …) declare [fs_walk] — distinct from [fs_read]
2334 // (which is content reads via io.read), so reviewers can
2335 // separately track directory traversal vs file-content
2336 // exposure. Mutating ops (mkdir_p, remove, copy) declare
2337 // [fs_write]. Path scoping uses --allow-fs-read for walk
2338 // (a directory listing is an information disclosure on
2339 // the same path tree) and --allow-fs-write for mutations.
2340 let stat_t = || {
2341 let mut fs = IndexMap::new();
2342 fs.insert("size".into(), Ty::int());
2343 fs.insert("mtime".into(), Ty::int());
2344 fs.insert("is_dir".into(), Ty::bool());
2345 fs.insert("is_file".into(), Ty::bool());
2346 Ty::Record(fs)
2347 };
2348 let result_str = |t: Ty| Ty::Con("Result".into(), vec![t, Ty::str()]);
2349 let mut fields = IndexMap::new();
2350 // Content read/write [fs_read] / [fs_write]
2351 //
2352 // These are what make [fs_read] an effect a program can
2353 // actually produce. Before them the only content I/O in the
2354 // language was io.read / io.write under [io], so "does this
2355 // touch the filesystem" was not answerable from an effect row
2356 // and `lex audit --effect fs_read` matched almost nothing
2357 // (#882). Path scoping is unchanged — the same
2358 // --allow-fs-read / --allow-fs-write allowlists, via the same
2359 // handler helpers the io.* ops now call.
2360 //
2361 // fs.read_to_string :: Str -> [fs_read] Result[Str, Str]
2362 fields.insert("read_to_string".into(), Ty::function(
2363 vec![Ty::str()],
2364 EffectSet::singleton("fs_read"),
2365 result_str(Ty::str())));
2366 // fs.write :: (Str, Str) -> [fs_write] Result[Unit, Str]
2367 fields.insert("write".into(), Ty::function(
2368 vec![Ty::str(), Ty::str()],
2369 EffectSet::singleton("fs_write"),
2370 result_str(Ty::Unit)));
2371 // fs.append :: (Str, Str) -> [fs_write] Result[Unit, Str]
2372 //
2373 // Without this, adding a line to a file means read-all,
2374 // concatenate, write-all — so an append-only log costs O(n) bytes
2375 // per entry and O(n^2) over its life. Measured on a running
2376 // hash-chained ledger: 1.8 TB written in a week to store 87 MB
2377 // (#899). Sharding into many files trades a write problem for a
2378 // correctness one, and reaching for a database to add a line to a
2379 // log is a service dependency inside programs whose argument is a
2380 // small auditable surface.
2381 //
2382 // Same effect as `write` and the same `--allow-fs-write` gate: it
2383 // modifies a file, and nothing about appending makes it need less
2384 // authority. It deliberately does NOT imply read — an appender
2385 // that cannot read what it writes to is a genuinely smaller
2386 // authority for a log-only component.
2387 fields.insert("append".into(), Ty::function(
2388 vec![Ty::str(), Ty::str()],
2389 EffectSet::singleton("fs_write"),
2390 result_str(Ty::Unit)));
2391
2392 // Walk-style queries [fs_walk]
2393 fields.insert("exists".into(), Ty::function(
2394 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2395 fields.insert("is_file".into(), Ty::function(
2396 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2397 fields.insert("is_dir".into(), Ty::function(
2398 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2399 fields.insert("stat".into(), Ty::function(
2400 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2401 result_str(stat_t())));
2402 fields.insert("list_dir".into(), Ty::function(
2403 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2404 result_str(Ty::List(Box::new(Ty::str())))));
2405 fields.insert("walk".into(), Ty::function(
2406 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2407 result_str(Ty::List(Box::new(Ty::str())))));
2408 fields.insert("glob".into(), Ty::function(
2409 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2410 result_str(Ty::List(Box::new(Ty::str())))));
2411 // Mutations [fs_write]
2412 fields.insert("mkdir_p".into(), Ty::function(
2413 vec![Ty::str()], EffectSet::singleton("fs_write"),
2414 result_str(Ty::Unit)));
2415 fields.insert("remove".into(), Ty::function(
2416 vec![Ty::str()], EffectSet::singleton("fs_write"),
2417 result_str(Ty::Unit)));
2418 fields.insert("copy".into(), Ty::function(
2419 vec![Ty::str(), Ty::str()],
2420 EffectSet {
2421 concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2422 var: None,
2423 },
2424 result_str(Ty::Unit)));
2425 Some(Ty::Record(fields))
2426 }
2427 "kv" => {
2428 // Embedded key-value store. The opaque `Kv` type is
2429 // backed by an Int handle into a process-wide registry.
2430 let kv_t = || Ty::Con("Kv".into(), vec![]);
2431 let mut fields = IndexMap::new();
2432 // open :: Str -> [kv, fs_write] Result[Kv, Str]
2433 fields.insert("open".into(), Ty::function(
2434 vec![Ty::str()],
2435 EffectSet {
2436 concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2437 var: None,
2438 },
2439 Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
2440 // close :: Kv -> [kv] Unit
2441 fields.insert("close".into(), Ty::function(
2442 vec![kv_t()],
2443 EffectSet::singleton("kv"),
2444 Ty::Unit));
2445 // get :: Kv, Str -> [kv] Option[Bytes]
2446 fields.insert("get".into(), Ty::function(
2447 vec![kv_t(), Ty::str()],
2448 EffectSet::singleton("kv"),
2449 Ty::Con("Option".into(), vec![Ty::bytes()])));
2450 // put :: Kv, Str, Bytes -> [kv] Result[Unit, Str]
2451 fields.insert("put".into(), Ty::function(
2452 vec![kv_t(), Ty::str(), Ty::bytes()],
2453 EffectSet::singleton("kv"),
2454 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2455 // delete :: Kv, Str -> [kv] Result[Unit, Str]
2456 fields.insert("delete".into(), Ty::function(
2457 vec![kv_t(), Ty::str()],
2458 EffectSet::singleton("kv"),
2459 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2460 // contains :: Kv, Str -> [kv] Bool
2461 fields.insert("contains".into(), Ty::function(
2462 vec![kv_t(), Ty::str()],
2463 EffectSet::singleton("kv"),
2464 Ty::bool()));
2465 // list_prefix :: Kv, Str -> [kv] List[Str]
2466 fields.insert("list_prefix".into(), Ty::function(
2467 vec![kv_t(), Ty::str()],
2468 EffectSet::singleton("kv"),
2469 Ty::List(Box::new(Ty::str()))));
2470 Some(Ty::Record(fields))
2471 }
2472 "moe" => {
2473 // MoE expert-store placement ops (lex-moe#25). Native
2474 // EffectHandler dispatch on the `moe` kind — see
2475 // lex-moe's crates/moe-policy/src/lib.rs `MoeHost` for
2476 // the Rust-side implementation these signatures describe.
2477 // A single process-wide tier cache backs every op, so
2478 // unlike `kv` there's no open/close handle to thread.
2479 let usage_entry_t = || {
2480 let mut f = IndexMap::new();
2481 f.insert("hash".into(), Ty::str());
2482 f.insert("size".into(), Ty::int());
2483 f.insert("temp".into(), Ty::float());
2484 f.insert("hits".into(), Ty::int());
2485 f.insert("loads".into(), Ty::int());
2486 Ty::Record(f)
2487 };
2488 let stats_t = || {
2489 let mut f = IndexMap::new();
2490 f.insert("hits".into(), Ty::int());
2491 f.insert("misses".into(), Ty::int());
2492 f.insert("prefetched".into(), Ty::int());
2493 f.insert("evictions".into(), Ty::int());
2494 f.insert("resident_bytes".into(), Ty::int());
2495 Ty::Record(f)
2496 };
2497 let mut fields = IndexMap::new();
2498 // pin(hash :: Str) -> [moe] Unit
2499 fields.insert("pin".into(), Ty::function(
2500 vec![Ty::str()],
2501 EffectSet::singleton("moe"),
2502 Ty::Unit));
2503 // unpin(hash :: Str) -> [moe] Unit
2504 fields.insert("unpin".into(), Ty::function(
2505 vec![Ty::str()],
2506 EffectSet::singleton("moe"),
2507 Ty::Unit));
2508 // prefetch_hint(hashes :: List[Str]) -> [moe] Int (loads spawned)
2509 fields.insert("prefetch_hint".into(), Ty::function(
2510 vec![Ty::List(Box::new(Ty::str()))],
2511 EffectSet::singleton("moe"),
2512 Ty::int()));
2513 // usage_snapshot() -> [moe] List[{hash, size, temp, hits, loads}]
2514 fields.insert("usage_snapshot".into(), Ty::function(
2515 vec![],
2516 EffectSet::singleton("moe"),
2517 Ty::List(Box::new(usage_entry_t()))));
2518 // stats() -> [moe] {hits, misses, prefetched, evictions, resident_bytes}
2519 fields.insert("stats".into(), Ty::function(
2520 vec![],
2521 EffectSet::singleton("moe"),
2522 stats_t()));
2523 Some(Ty::Record(fields))
2524 }
2525 "vcs" => {
2526 // Content-addressed blob store (#5 / M6.1b). `put_blob` returns the
2527 // lowercase hex SHA-256 of the content — the SAME id as
2528 // `crypto.sha256_str`, so blobs are interchangeable with loom's
2529 // SQLite-backed artifacts by id. `ref_set`/`ref_get` bind a name
2530 // (namespace + key) to a blob sha, e.g. namespace "loom/sprint-{id}",
2531 // key = node id (branch-per-sprint, #5). The on-disk layout matches
2532 // lex-store's blob CAS (<root>/blobs, <root>/blobrefs).
2533 //
2534 // fs_write/fs_read appear in the effect rows so `lex audit` sees the
2535 // disk touch; the store root is internal (~/.lex/store or
2536 // $LEX_STORE_ROOT) so no user-path allowlist applies.
2537 let mut fields = IndexMap::new();
2538 let vcs_w = || EffectSet {
2539 concrete: [crate::types::EffectKind::bare("vcs"),
2540 crate::types::EffectKind::bare("fs_write")]
2541 .into_iter().collect(),
2542 var: None,
2543 };
2544 let vcs_r = || EffectSet {
2545 concrete: [crate::types::EffectKind::bare("vcs"),
2546 crate::types::EffectKind::bare("fs_read")]
2547 .into_iter().collect(),
2548 var: None,
2549 };
2550 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
2551
2552 // put_blob :: Str -> [vcs, fs_write] Result[Str, Str] (returns sha)
2553 fields.insert("put_blob".into(), Ty::function(
2554 vec![Ty::str()], vcs_w(), res(Ty::str())));
2555 // get_blob :: Str -> [vcs, fs_read] Result[Str, Str]
2556 fields.insert("get_blob".into(), Ty::function(
2557 vec![Ty::str()], vcs_r(), res(Ty::str())));
2558 // has_blob :: Str -> [vcs, fs_read] Bool
2559 fields.insert("has_blob".into(), Ty::function(
2560 vec![Ty::str()], vcs_r(), Ty::bool()));
2561 // ref_set :: Str, Str, Str -> [vcs, fs_write] Result[Unit, Str]
2562 fields.insert("ref_set".into(), Ty::function(
2563 vec![Ty::str(), Ty::str(), Ty::str()], vcs_w(), res(Ty::Unit)));
2564 // ref_get :: Str, Str -> [vcs, fs_read] Result[Str, Str] (key -> sha)
2565 fields.insert("ref_get".into(), Ty::function(
2566 vec![Ty::str(), Ty::str()], vcs_r(), res(Ty::str())));
2567 Some(Ty::Record(fields))
2568 }
2569 "sql" => {
2570 // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
2571 // backed by an Int handle into a process-wide registry (#362).
2572 //
2573 // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
2574 // registered in env.rs, so callers don't have to stringify values.
2575 //
2576 // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
2577 // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
2578 //
2579 // Row decoders: get_str / get_int / get_float / get_bool extract
2580 // typed columns from a row record by name.
2581 let db_t = || Ty::Con("Db".into(), vec![]);
2582 let tx_t = || Ty::Con("SqlTx".into(), vec![]);
2583 let sp_t = || Ty::Con("SqlParam".into(), vec![]);
2584 let params_t = || Ty::List(Box::new(sp_t()));
2585 let mut fields = IndexMap::new();
2586
2587 // SqlError = { message, code, detail } — populated with
2588 // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
2589 let se_t = || Ty::Con("SqlError".into(), vec![]);
2590
2591 // open :: Str -> [sql, fs_write] Result[Db, SqlError]
2592 fields.insert("open".into(), Ty::function(
2593 vec![Ty::str()],
2594 EffectSet {
2595 concrete: [crate::types::EffectKind::bare("sql"),
2596 crate::types::EffectKind::bare("fs_write")]
2597 .into_iter().collect(),
2598 var: None,
2599 },
2600 Ty::Con("Result".into(), vec![db_t(), se_t()])));
2601
2602 // close :: Db -> [sql] Unit
2603 fields.insert("close".into(), Ty::function(
2604 vec![db_t()],
2605 EffectSet::singleton("sql"),
2606 Ty::Unit));
2607
2608 // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2609 fields.insert("exec".into(), Ty::function(
2610 vec![db_t(), Ty::str(), params_t()],
2611 EffectSet::singleton("sql"),
2612 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2613
2614 // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2615 fields.insert("query".into(), Ty::function(
2616 vec![db_t(), Ty::str(), params_t()],
2617 EffectSet::singleton("sql"),
2618 Ty::Con("Result".into(), vec![
2619 Ty::List(Box::new(Ty::Var(0))),
2620 se_t(),
2621 ])));
2622
2623 // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
2624 // Streaming variant of `query` (#379). Rows are pulled from
2625 // the server one at a time via an mpsc-backed cursor —
2626 // memory stays bounded regardless of result-set size.
2627 // Other ops on the same `Db` handle block until the cursor
2628 // is drained (single connection per Db).
2629 fields.insert("query_iter".into(), Ty::function(
2630 vec![db_t(), Ty::str(), params_t()],
2631 EffectSet::singleton("sql"),
2632 Ty::Con("Result".into(), vec![
2633 Ty::Con("Iter".into(), vec![Ty::Var(0)]),
2634 se_t(),
2635 ])));
2636
2637 // begin :: Db -> [sql] Result[SqlTx, SqlError]
2638 fields.insert("begin".into(), Ty::function(
2639 vec![db_t()],
2640 EffectSet::singleton("sql"),
2641 Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2642
2643 // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2644 fields.insert("commit".into(), Ty::function(
2645 vec![tx_t()],
2646 EffectSet::singleton("sql"),
2647 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2648
2649 // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2650 fields.insert("rollback".into(), Ty::function(
2651 vec![tx_t()],
2652 EffectSet::singleton("sql"),
2653 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2654
2655 // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2656 fields.insert("exec_tx".into(), Ty::function(
2657 vec![tx_t(), Ty::str(), params_t()],
2658 EffectSet::singleton("sql"),
2659 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2660
2661 // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2662 fields.insert("query_tx".into(), Ty::function(
2663 vec![tx_t(), Ty::str(), params_t()],
2664 EffectSet::singleton("sql"),
2665 Ty::Con("Result".into(), vec![
2666 Ty::List(Box::new(Ty::Var(0))),
2667 se_t(),
2668 ])));
2669
2670 // Row decoders: get_X[T] :: T, Str -> Option[X]
2671 // T is polymorphic so these work on any row record shape.
2672 fields.insert("get_str".into(), Ty::function(
2673 vec![Ty::Var(0), Ty::str()],
2674 EffectSet::empty(),
2675 Ty::Con("Option".into(), vec![Ty::str()])));
2676 fields.insert("get_int".into(), Ty::function(
2677 vec![Ty::Var(0), Ty::str()],
2678 EffectSet::empty(),
2679 Ty::Con("Option".into(), vec![Ty::int()])));
2680 fields.insert("get_float".into(), Ty::function(
2681 vec![Ty::Var(0), Ty::str()],
2682 EffectSet::empty(),
2683 Ty::Con("Option".into(), vec![Ty::float()])));
2684 fields.insert("get_bool".into(), Ty::function(
2685 vec![Ty::Var(0), Ty::str()],
2686 EffectSet::empty(),
2687 Ty::Con("Option".into(), vec![Ty::bool()])));
2688
2689 Some(Ty::Record(fields))
2690 }
2691 "redis" => {
2692 // Thin Redis client (#533). ConnRedis is an opaque handle backed by a
2693 // process-wide registry (same pattern as Db in std.sql). All ops carry
2694 // [net] — Redis is a TCP service; no separate [redis] effect.
2695 //
2696 // subscribe / psubscribe return Unit because they are blocking
2697 // infinite loops, consistent with net.serve_fn and ws.serve.
2698 //
2699 // subscribe/psubscribe open a *dedicated* connection internally —
2700 // Redis disallows non-Pub/Sub commands on a subscribed connection.
2701 let conn_t = || Ty::Con("ConnRedis".into(), vec![]);
2702 let mut fields = IndexMap::new();
2703
2704 // connect :: Str -> [net] Result[ConnRedis, Str]
2705 // url: "redis://host:6379" or "rediss://host:6380" (TLS)
2706 fields.insert("connect".into(), Ty::function(
2707 vec![Ty::str()],
2708 EffectSet::singleton("net"),
2709 Ty::Con("Result".into(), vec![conn_t(), Ty::str()])));
2710
2711 // close :: ConnRedis -> [net] Unit
2712 fields.insert("close".into(), Ty::function(
2713 vec![conn_t()],
2714 EffectSet::singleton("net"),
2715 Ty::Unit));
2716
2717 // ---- Key-value -----------------------------------------------
2718
2719 // get :: ConnRedis, Str -> [net] Option[Str]
2720 fields.insert("get".into(), Ty::function(
2721 vec![conn_t(), Ty::str()],
2722 EffectSet::singleton("net"),
2723 Ty::Con("Option".into(), vec![Ty::str()])));
2724
2725 // set :: ConnRedis, Str, Str -> [net] Unit
2726 fields.insert("set".into(), Ty::function(
2727 vec![conn_t(), Ty::str(), Ty::str()],
2728 EffectSet::singleton("net"),
2729 Ty::Unit));
2730
2731 // set_ex :: ConnRedis, Str, Str, Int -> [net] Unit
2732 fields.insert("set_ex".into(), Ty::function(
2733 vec![conn_t(), Ty::str(), Ty::str(), Ty::int()],
2734 EffectSet::singleton("net"),
2735 Ty::Unit));
2736
2737 // del :: ConnRedis, Str -> [net] Unit
2738 fields.insert("del".into(), Ty::function(
2739 vec![conn_t(), Ty::str()],
2740 EffectSet::singleton("net"),
2741 Ty::Unit));
2742
2743 // exists :: ConnRedis, Str -> [net] Bool
2744 fields.insert("exists".into(), Ty::function(
2745 vec![conn_t(), Ty::str()],
2746 EffectSet::singleton("net"),
2747 Ty::bool()));
2748
2749 // expire :: ConnRedis, Str, Int -> [net] Unit
2750 fields.insert("expire".into(), Ty::function(
2751 vec![conn_t(), Ty::str(), Ty::int()],
2752 EffectSet::singleton("net"),
2753 Ty::Unit));
2754
2755 // ---- Pub/Sub -------------------------------------------------
2756
2757 // publish :: ConnRedis, Str, Str -> [net] Int
2758 // Returns the number of subscribers that received the message.
2759 fields.insert("publish".into(), Ty::function(
2760 vec![conn_t(), Ty::str(), Ty::str()],
2761 EffectSet::singleton("net"),
2762 Ty::int()));
2763
2764 // subscribe :: ConnRedis, Str, (Str, Str ->[E] Unit) -> [net] Unit
2765 // Blocking loop; handler receives (channel, message) on each message.
2766 // Uses a dedicated connection — Redis disallows non-Pub/Sub commands
2767 // on a subscribed connection. Handler carries an open effect row so
2768 // callers can use io, net, sql, etc. inside the closure.
2769 let handler2 = Ty::function(
2770 vec![Ty::str(), Ty::str()],
2771 EffectSet::open_var(0),
2772 Ty::Unit);
2773 fields.insert("subscribe".into(), Ty::function(
2774 vec![conn_t(), Ty::str(), handler2],
2775 EffectSet::singleton("net"),
2776 Ty::Unit)); // Unit
2777
2778 // psubscribe :: ConnRedis, Str, (Str, Str, Str ->[E] Unit) -> [net] Unit
2779 // Pattern-subscribe; handler receives (pattern, channel, message).
2780 // Handler carries an open effect row (same rationale as subscribe).
2781 let handler3 = Ty::function(
2782 vec![Ty::str(), Ty::str(), Ty::str()],
2783 EffectSet::open_var(1),
2784 Ty::Unit);
2785 fields.insert("psubscribe".into(), Ty::function(
2786 vec![conn_t(), Ty::str(), handler3],
2787 EffectSet::singleton("net"),
2788 Ty::Unit)); // Unit
2789
2790 // ---- List ----------------------------------------------------
2791
2792 // lpush :: ConnRedis, Str, Str -> [net] Int
2793 fields.insert("lpush".into(), Ty::function(
2794 vec![conn_t(), Ty::str(), Ty::str()],
2795 EffectSet::singleton("net"),
2796 Ty::int()));
2797
2798 // rpush :: ConnRedis, Str, Str -> [net] Int
2799 fields.insert("rpush".into(), Ty::function(
2800 vec![conn_t(), Ty::str(), Ty::str()],
2801 EffectSet::singleton("net"),
2802 Ty::int()));
2803
2804 // brpop :: ConnRedis, Str, Int -> [net] Option[Str]
2805 // Blocking right-pop; returns None on timeout. timeout=0 blocks
2806 // indefinitely (the runtime does not treat this as a hung effect).
2807 fields.insert("brpop".into(), Ty::function(
2808 vec![conn_t(), Ty::str(), Ty::int()],
2809 EffectSet::singleton("net"),
2810 Ty::Con("Option".into(), vec![Ty::str()])));
2811
2812 // llen :: ConnRedis, Str -> [net] Int
2813 fields.insert("llen".into(), Ty::function(
2814 vec![conn_t(), Ty::str()],
2815 EffectSet::singleton("net"),
2816 Ty::int()));
2817
2818 // ---- Hash ----------------------------------------------------
2819
2820 // hset :: ConnRedis, Str, Str, Str -> [net] Unit
2821 fields.insert("hset".into(), Ty::function(
2822 vec![conn_t(), Ty::str(), Ty::str(), Ty::str()],
2823 EffectSet::singleton("net"),
2824 Ty::Unit));
2825
2826 // hget :: ConnRedis, Str, Str -> [net] Option[Str]
2827 fields.insert("hget".into(), Ty::function(
2828 vec![conn_t(), Ty::str(), Ty::str()],
2829 EffectSet::singleton("net"),
2830 Ty::Con("Option".into(), vec![Ty::str()])));
2831
2832 // hdel :: ConnRedis, Str, Str -> [net] Unit
2833 fields.insert("hdel".into(), Ty::function(
2834 vec![conn_t(), Ty::str(), Ty::str()],
2835 EffectSet::singleton("net"),
2836 Ty::Unit));
2837
2838 // hgetall :: ConnRedis, Str -> [net] List[(Str, Str)]
2839 fields.insert("hgetall".into(), Ty::function(
2840 vec![conn_t(), Ty::str()],
2841 EffectSet::singleton("net"),
2842 Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), Ty::str()])))));
2843
2844 Some(Ty::Record(fields))
2845 }
2846 "parser" => {
2847 // #217: structured parser combinators. Parser values are
2848 // tagged Records at runtime (`{ kind, ... }`), opaque at
2849 // the language level via `Ty::Con("Parser", [T])`.
2850 //
2851 // Surface:
2852 // - primitives: char, string, digit, alpha, whitespace, eof
2853 // - combinators: seq, alt, many, optional, map, and_then
2854 // - run :: Parser[T], Str -> Result[T, ParseErr]
2855 //
2856 // `map` and `and_then` were deferred from #217's v1 because
2857 // their closure arguments carried call-site identity that
2858 // broke the canonical-parsers acceptance criterion. With
2859 // closure body-hash equality landed in #222, that concern
2860 // is gone, and #221 wires them in. The interpreter for
2861 // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2862 // so it can invoke closures from `Map` / `AndThen` nodes.
2863 let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2864 let parse_err = || {
2865 let mut fs = IndexMap::new();
2866 fs.insert("pos".into(), Ty::int());
2867 fs.insert("message".into(), Ty::str());
2868 Ty::Record(fs)
2869 };
2870 let mut fields = IndexMap::new();
2871 // char :: Str -> Parser[Str] (single-char Str literal)
2872 fields.insert("char".into(), Ty::function(
2873 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2874 // string :: Str -> Parser[Str]
2875 fields.insert("string".into(), Ty::function(
2876 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2877 // digit :: () -> Parser[Str]
2878 fields.insert("digit".into(), Ty::function(
2879 vec![], EffectSet::empty(), pt(Ty::str())));
2880 // alpha :: () -> Parser[Str]
2881 fields.insert("alpha".into(), Ty::function(
2882 vec![], EffectSet::empty(), pt(Ty::str())));
2883 // whitespace :: () -> Parser[Str]
2884 fields.insert("whitespace".into(), Ty::function(
2885 vec![], EffectSet::empty(), pt(Ty::str())));
2886 // eof :: () -> Parser[Unit]
2887 fields.insert("eof".into(), Ty::function(
2888 vec![], EffectSet::empty(), pt(Ty::Unit)));
2889 // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2890 fields.insert("seq".into(), Ty::function(
2891 vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2892 EffectSet::empty(),
2893 pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2894 // alt :: Parser[T], Parser[T] -> Parser[T]
2895 // PEG-style ordered choice: the second alternative is
2896 // tried only if the first fails.
2897 fields.insert("alt".into(), Ty::function(
2898 vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2899 EffectSet::empty(),
2900 pt(Ty::Var(0))));
2901 // many :: Parser[T] -> Parser[List[T]]
2902 // Zero-or-more. Stops as soon as the inner parser fails
2903 // OR doesn't advance the position (avoids infinite loop
2904 // on empty matches).
2905 fields.insert("many".into(), Ty::function(
2906 vec![pt(Ty::Var(0))],
2907 EffectSet::empty(),
2908 pt(Ty::List(Box::new(Ty::Var(0))))));
2909 // optional :: Parser[T] -> Parser[Option[T]]
2910 fields.insert("optional".into(), Ty::function(
2911 vec![pt(Ty::Var(0))],
2912 EffectSet::empty(),
2913 pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2914 // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2915 // The closure runs at parse time when the Parser is run.
2916 // Effect-polymorphic on the closure: any effect the
2917 // closure declares propagates to the surrounding `run`.
2918 fields.insert("map".into(), Ty::function(
2919 vec![
2920 pt(Ty::Var(0)),
2921 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2922 ],
2923 EffectSet::open_var(2),
2924 pt(Ty::Var(1))));
2925 // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2926 // Monadic bind: closure inspects the parsed value and
2927 // returns the next parser to run.
2928 fields.insert("and_then".into(), Ty::function(
2929 vec![
2930 pt(Ty::Var(0)),
2931 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2932 pt(Ty::Var(1))),
2933 ],
2934 EffectSet::open_var(3),
2935 pt(Ty::Var(1))));
2936 // run :: Parser[T], Str -> Result[T, ParseErr]
2937 // ParseErr = { pos :: Int, message :: Str }
2938 fields.insert("run".into(), Ty::function(
2939 vec![pt(Ty::Var(0)), Ty::str()],
2940 EffectSet::empty(),
2941 Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2942 Some(Ty::Record(fields))
2943 }
2944 "cli" => {
2945 // #224 Rubric port: argparse-equivalent for end-user
2946 // programs. Spec values are tagged `Json` records (opaque
2947 // to the language but inspectable). Construction via the
2948 // `flag` / `option` / `positional` / `spec` builders;
2949 // parse + introspection / help via the remaining ops.
2950 let json = || Ty::Con("Json".into(), vec![]);
2951 let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2952 let mut fields = IndexMap::new();
2953 // flag :: Str -> Option[Str] -> Str -> Json
2954 // long_name -> short -> help -> CliArg
2955 fields.insert("flag".into(), Ty::function(
2956 vec![Ty::str(), opt_str(), Ty::str()],
2957 EffectSet::empty(),
2958 json()));
2959 // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2960 // long_name -> short -> help -> default -> CliArg
2961 fields.insert("option".into(), Ty::function(
2962 vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2963 EffectSet::empty(),
2964 json()));
2965 // positional :: Str -> Str -> Bool -> Json
2966 // name -> help -> required -> CliArg
2967 fields.insert("positional".into(), Ty::function(
2968 vec![Ty::str(), Ty::str(), Ty::bool()],
2969 EffectSet::empty(),
2970 json()));
2971 // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2972 // name -> help -> args -> subcommands -> CliSpec
2973 fields.insert("spec".into(), Ty::function(
2974 vec![Ty::str(), Ty::str(),
2975 Ty::List(Box::new(json())),
2976 Ty::List(Box::new(json()))],
2977 EffectSet::empty(),
2978 json()));
2979 // parse :: Json -> List[Str] -> Result[Json, Str]
2980 // spec -> argv -> Result[CliParsed, error]
2981 fields.insert("parse".into(), Ty::function(
2982 vec![json(), Ty::List(Box::new(Ty::str()))],
2983 EffectSet::empty(),
2984 Ty::Con("Result".into(), vec![json(), Ty::str()])));
2985 // envelope :: Bool -> Str -> T -> Json
2986 // ok -> command -> data -> ACLI-shaped envelope.
2987 // `data` is polymorphic so callers don't have to round-
2988 // trip through `json.parse` for trivial payloads.
2989 fields.insert("envelope".into(), Ty::function(
2990 vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2991 EffectSet::empty(),
2992 json()));
2993 // describe :: Json -> Json — machine-readable spec dump
2994 fields.insert("describe".into(), Ty::function(
2995 vec![json()],
2996 EffectSet::empty(),
2997 json()));
2998 // help :: Json -> Str — human-readable help text
2999 fields.insert("help".into(), Ty::function(
3000 vec![json()],
3001 EffectSet::empty(),
3002 Ty::str()));
3003 Some(Ty::Record(fields))
3004 }
3005 "regex" => {
3006 // The compiled `Regex` is stored as a `Str` at runtime
3007 // (the pattern source) plus a process-wide cache of the
3008 // actual `regex::Regex`. So `Regex` is a nominal type at
3009 // the language level but its value is just the pattern.
3010 let regex_t = || Ty::Con("Regex".into(), vec![]);
3011 let match_t = || {
3012 let mut fs = IndexMap::new();
3013 fs.insert("text".into(), Ty::str());
3014 fs.insert("start".into(), Ty::int());
3015 fs.insert("end".into(), Ty::int());
3016 fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
3017 Ty::Record(fs)
3018 };
3019 let mut fields = IndexMap::new();
3020 // compile :: Str -> Result[Regex, Str]
3021 fields.insert("compile".into(), Ty::function(
3022 vec![Ty::str()], EffectSet::empty(),
3023 Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
3024 // is_match :: Regex, Str -> Bool
3025 fields.insert("is_match".into(), Ty::function(
3026 vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
3027 // is_match_str :: Str, Str -> Bool
3028 // Compiles the first argument as a pattern and matches against the second.
3029 // Returns false on invalid pattern instead of propagating an error.
3030 fields.insert("is_match_str".into(), Ty::function(
3031 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
3032 // find :: Regex, Str -> Option[Match]
3033 fields.insert("find".into(), Ty::function(
3034 vec![regex_t(), Ty::str()], EffectSet::empty(),
3035 Ty::Con("Option".into(), vec![match_t()])));
3036 // find_all :: Regex, Str -> List[Match]
3037 fields.insert("find_all".into(), Ty::function(
3038 vec![regex_t(), Ty::str()], EffectSet::empty(),
3039 Ty::List(Box::new(match_t()))));
3040 // replace :: Regex, Str, Str -> Str
3041 fields.insert("replace".into(), Ty::function(
3042 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
3043 // replace_all :: Regex, Str, Str -> Str
3044 fields.insert("replace_all".into(), Ty::function(
3045 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
3046 // split :: Regex, Str -> List[Str]
3047 fields.insert("split".into(), Ty::function(
3048 vec![regex_t(), Ty::str()], EffectSet::empty(),
3049 Ty::List(Box::new(Ty::str()))));
3050 Some(Ty::Record(fields))
3051 }
3052 "http" => {
3053 // Rich HTTP client. `[net]` for the wire ops, pure for
3054 // the builders / decoders. `--allow-net-host` gates per
3055 // request. Multipart upload + streaming response bodies
3056 // are deferred to v1.5; the v1 surface covers the
3057 // common cases (auth, headers, query, timeouts, JSON /
3058 // text decoding).
3059 let req_t = || Ty::Con("HttpRequest".into(), vec![]);
3060 let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
3061 let err_t = || Ty::Con("HttpError".into(), vec![]);
3062 let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
3063 let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
3064 let mut fields = IndexMap::new();
3065 // -- wire ops (effectful) --
3066 // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
3067 fields.insert("send".into(), Ty::function(
3068 vec![req_t()],
3069 EffectSet::singleton("net"),
3070 result_he(resp_t()),
3071 ));
3072 // get :: Str -> [net] Result[HttpResponse, HttpError]
3073 fields.insert("get".into(), Ty::function(
3074 vec![Ty::str()],
3075 EffectSet::singleton("net"),
3076 result_he(resp_t()),
3077 ));
3078 // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
3079 fields.insert("post".into(), Ty::function(
3080 vec![Ty::str(), Ty::bytes(), Ty::str()],
3081 EffectSet::singleton("net"),
3082 result_he(resp_t()),
3083 ));
3084 // -- pure builders (record transforms) --
3085 // with_header :: HttpRequest, Str, Str -> HttpRequest
3086 fields.insert("with_header".into(), Ty::function(
3087 vec![req_t(), Ty::str(), Ty::str()],
3088 EffectSet::empty(),
3089 req_t(),
3090 ));
3091 // with_auth :: HttpRequest, Str, Str -> HttpRequest
3092 // (Renders `<scheme> <token>` into the `Authorization`
3093 // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
3094 fields.insert("with_auth".into(), Ty::function(
3095 vec![req_t(), Ty::str(), Ty::str()],
3096 EffectSet::empty(),
3097 req_t(),
3098 ));
3099 // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
3100 // (Appends a `?k=v&...` query string; values are URL-
3101 // encoded so `&` / `=` / spaces in values don't escape.)
3102 fields.insert("with_query".into(), Ty::function(
3103 vec![req_t(), str_str_map()],
3104 EffectSet::empty(),
3105 req_t(),
3106 ));
3107 // with_timeout_ms :: HttpRequest, Int -> HttpRequest
3108 fields.insert("with_timeout_ms".into(), Ty::function(
3109 vec![req_t(), Ty::int()],
3110 EffectSet::empty(),
3111 req_t(),
3112 ));
3113 // -- pure decoders --
3114 // json_body[T] :: HttpResponse -> Result[T, HttpError]
3115 // Polymorphic on the parsed shape, matching `json.parse`.
3116 fields.insert("json_body".into(), Ty::function(
3117 vec![resp_t()],
3118 EffectSet::empty(),
3119 result_he(Ty::Var(0)),
3120 ));
3121 // text_body :: HttpResponse -> Result[Str, HttpError]
3122 fields.insert("text_body".into(), Ty::function(
3123 vec![resp_t()],
3124 EffectSet::empty(),
3125 result_he(Ty::str()),
3126 ));
3127 // stream_lines :: Str, Map[Str, Str], Str -> [net] Result[Stream[Str], Str]
3128 // Streaming HTTP POST that yields the response body line-by-line
3129 // for SSE / NDJSON endpoints. Returns a lazy `Stream[Str]` (#683):
3130 // each `stream.next` pulls exactly one line off the socket as it
3131 // arrives, so an endpoint that holds the connection open and emits
3132 // events over time is consumed incrementally instead of blocking
3133 // until close. Connection errors at request time surface as
3134 // `Err(Str)`; a mid-stream read error / close ends the stream
3135 // (next `stream.next` returns `None`). Consume with `std.stream`
3136 // (`stream.next` / `stream.collect`), which carries `[stream]`.
3137 fields.insert("stream_lines".into(), Ty::function(
3138 vec![
3139 Ty::str(),
3140 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
3141 Ty::str(),
3142 ],
3143 EffectSet::singleton("net"),
3144 Ty::Con("Result".into(), vec![
3145 Ty::Con("Stream".into(), vec![Ty::str()]),
3146 Ty::str(),
3147 ]),
3148 ));
3149 Some(Ty::Record(fields))
3150 }
3151 "yaml" => {
3152 // YAML config parser. Same shape as `std.toml`: parse
3153 // is polymorphic, output Value layout matches std.json
3154 // (Str/Int/Float/Bool/List/Record). Anchors and tags
3155 // are flattened by serde_yaml's deserializer.
3156 let mut fields = IndexMap::new();
3157 fields.insert("parse".into(), Ty::function(
3158 vec![Ty::str()], EffectSet::empty(),
3159 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3160 ));
3161 // Tactical fix for #168 — caller-supplied required-field
3162 // list. See std.json's parse_strict for context.
3163 fields.insert("parse_strict".into(), Ty::function(
3164 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3165 EffectSet::empty(),
3166 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3167 ));
3168 fields.insert("stringify".into(), Ty::function(
3169 vec![Ty::Var(0)], EffectSet::empty(),
3170 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3171 ));
3172 Some(Ty::Record(fields))
3173 }
3174 "dotenv" => {
3175 // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
3176 // Returns a map (not a polymorphic record) because
3177 // dotenv files don't carry shape — every value is a
3178 // string and keys aren't statically known.
3179 let mut fields = IndexMap::new();
3180 fields.insert("parse".into(), Ty::function(
3181 vec![Ty::str()], EffectSet::empty(),
3182 Ty::Con("Result".into(), vec![
3183 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
3184 Ty::str(),
3185 ]),
3186 ));
3187 Some(Ty::Record(fields))
3188 }
3189 "csv" => {
3190 // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
3191 // Header awareness is left to the caller — row 0 is
3192 // whatever the file has. A `parse_with_headers` that
3193 // returns List[Map[Str,Str]] is a natural follow-up.
3194 let row_ty = Ty::List(Box::new(Ty::str()));
3195 let rows_ty = Ty::List(Box::new(row_ty.clone()));
3196 let mut fields = IndexMap::new();
3197 fields.insert("parse".into(), Ty::function(
3198 vec![Ty::str()], EffectSet::empty(),
3199 Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
3200 ));
3201 fields.insert("stringify".into(), Ty::function(
3202 vec![rows_ty], EffectSet::empty(),
3203 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3204 ));
3205 Some(Ty::Record(fields))
3206 }
3207 "test" => {
3208 // Tiny assertion library (#proposed-stdlib). Each helper
3209 // returns Result[Unit, Str] so a test is itself a fn
3210 // returning Result. Callers compose suites in user code
3211 // (a List of (name, () -> Result[Unit, Str]) pairs +
3212 // list.fold to accumulate verdicts). Property generators
3213 // and a Rust-side Suite type are deferred to v2.
3214 let mut fields = IndexMap::new();
3215 // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
3216 // (T constrained equal by unification on the two args)
3217 let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
3218 fields.insert("assert_eq".into(), Ty::function(
3219 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3220 ));
3221 fields.insert("assert_ne".into(), Ty::function(
3222 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3223 ));
3224 fields.insert("assert_true".into(), Ty::function(
3225 vec![Ty::bool()], EffectSet::empty(), unit_result(),
3226 ));
3227 fields.insert("assert_false".into(), Ty::function(
3228 vec![Ty::bool()], EffectSet::empty(), unit_result(),
3229 ));
3230 Some(Ty::Record(fields))
3231 }
3232 "toml" => {
3233 // TOML config parser. Mirrors `std.json`'s shape: parse
3234 // is polymorphic so callers annotate the expected
3235 // record / list / scalar shape and the type checker
3236 // unifies. The parsed TOML maps to the same Lex Value
3237 // shape as JSON does:
3238 //
3239 // TOML String → Value::Str
3240 // TOML Integer → Value::Int
3241 // TOML Float → Value::Float
3242 // TOML Boolean → Value::Bool
3243 // TOML Array → Value::List
3244 // TOML Table → Value::Record
3245 // TOML Datetime → Value::Str (RFC 3339, lossless)
3246 //
3247 // The Datetime → Str fallback is the one info-losing
3248 // step; callers who want a real `Instant` can pipe the
3249 // string through `datetime.parse_iso`.
3250 let mut fields = IndexMap::new();
3251 // parse :: Str -> Result[T, Str]
3252 fields.insert("parse".into(), Ty::function(
3253 vec![Ty::str()], EffectSet::empty(),
3254 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3255 ));
3256 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
3257 // Tactical fix for #168 — caller passes the field
3258 // names T requires; runtime returns Err if any are
3259 // missing from the parsed table instead of letting
3260 // field access panic later.
3261 fields.insert("parse_strict".into(), Ty::function(
3262 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3263 EffectSet::empty(),
3264 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3265 ));
3266 // stringify :: T -> Result[Str, Str]
3267 // Returns Result (not Str) because not every Lex Value
3268 // has a TOML representation — top-level scalars,
3269 // closures, mixed-key maps etc. surface as Err rather
3270 // than panic.
3271 fields.insert("stringify".into(), Ty::function(
3272 vec![Ty::Var(0)], EffectSet::empty(),
3273 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3274 ));
3275 Some(Ty::Record(fields))
3276 }
3277 // `std.agent` (#184) — runtime primitives whose effects
3278 // separate (a) which LLM surface (`llm_local` vs
3279 // `llm_cloud`), (b) which peer protocol (`a2a`), and
3280 // (c) which tool boundary (`mcp`). The wire formats land
3281 // in downstream crates (`soft-agent`, `soft-a2a`) and
3282 // in #185 for MCP; what's typed here is the boundary
3283 // alone — agent code can be type-checked as
3284 // `[llm_local, a2a]` and will fail if it tries to reach
3285 // `[llm_cloud]` even before the wire layer is finished.
3286 "agent" => {
3287 let mut fields = IndexMap::new();
3288 // local_complete :: Str -> [llm_local] Result[Str, Str]
3289 fields.insert("local_complete".into(), Ty::function(
3290 vec![Ty::str()],
3291 EffectSet::singleton("llm_local"),
3292 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3293 ));
3294 // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
3295 fields.insert("cloud_complete".into(), Ty::function(
3296 vec![Ty::str()],
3297 EffectSet::singleton("llm_cloud"),
3298 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3299 ));
3300 // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
3301 // peer payload reply
3302 fields.insert("send_a2a".into(), Ty::function(
3303 vec![Ty::str(), Ty::str()],
3304 EffectSet::singleton("a2a"),
3305 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3306 ));
3307 // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
3308 // server tool args_json result_json
3309 fields.insert("call_mcp".into(), Ty::function(
3310 vec![Ty::str(), Ty::str(), Ty::str()],
3311 EffectSet::singleton("mcp"),
3312 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3313 ));
3314 // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
3315 // (#305 slice 3). Streaming counterpart to cloud_complete.
3316 // The result is `Result[Stream[Str], Str]` rather than a
3317 // bare Stream so transport errors surface synchronously
3318 // at handshake time; per-chunk errors collapse the
3319 // stream to early termination.
3320 fields.insert("cloud_stream".into(), Ty::function(
3321 vec![Ty::str()],
3322 EffectSet::singleton("llm_cloud"),
3323 Ty::Con("Result".into(), vec![
3324 Ty::Con("Stream".into(), vec![Ty::str()]),
3325 Ty::str(),
3326 ]),
3327 ));
3328 Some(Ty::Record(fields))
3329 }
3330 "stream" => {
3331 // #305 slice 3: opaque consumer-side operations on
3332 // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
3333 // for now); future producers (`http.get_stream`, etc.)
3334 // will register the same Stream[T] surface.
3335 let mut fields = IndexMap::new();
3336 // next :: Stream[T] -> [stream] Option[T]
3337 // One pull. `None` signals end-of-stream (consumed by
3338 // the producer's lazy generator).
3339 fields.insert("next".into(), Ty::function(
3340 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3341 EffectSet::singleton("stream"),
3342 Ty::Con("Option".into(), vec![Ty::Var(0)]),
3343 ));
3344 // collect :: Stream[T] -> [stream] List[T]
3345 // Drain to a list. Eager; blocks until the producer
3346 // signals end-of-stream.
3347 fields.insert("collect".into(), Ty::function(
3348 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3349 EffectSet::singleton("stream"),
3350 Ty::List(Box::new(Ty::Var(0))),
3351 ));
3352 Some(Ty::Record(fields))
3353 }
3354 // -- std.decimal (#574): exact decimal arithmetic with explicit rounding.
3355 // `Decimal = { coefficient :: Int, exponent :: Int }` where the value
3356 // is `coefficient × 10^exponent`. All arithmetic is exact (no IEEE 754
3357 // approximation); rounding only happens at `round_to`, which demands an
3358 // explicit mode string ("HalfUp" | "HalfDown" | "HalfEven" |
3359 // "Down" | "Up" | "Ceiling" | "Floor").
3360 "decimal" => {
3361 // Local helper: the Decimal record type.
3362 let decimal_ty = || {
3363 let mut f = IndexMap::new();
3364 f.insert("coefficient".into(), Ty::int());
3365 f.insert("exponent".into(), Ty::int());
3366 Ty::Record(f)
3367 };
3368 let mut fields = IndexMap::new();
3369 // Constructors
3370 // decimal :: (Int, Int) -> Decimal — coefficient, exponent
3371 fields.insert("decimal".into(), Ty::function(
3372 vec![Ty::int(), Ty::int()], EffectSet::empty(), decimal_ty()));
3373 // zero :: () -> Decimal — 0 × 10^0
3374 fields.insert("zero".into(), Ty::function(
3375 vec![], EffectSet::empty(), decimal_ty()));
3376 // one :: () -> Decimal — 1 × 10^0
3377 fields.insert("one".into(), Ty::function(
3378 vec![], EffectSet::empty(), decimal_ty()));
3379 // from_int :: Int -> Decimal — lift integer, exponent=0
3380 fields.insert("from_int".into(), Ty::function(
3381 vec![Ty::int()], EffectSet::empty(), decimal_ty()));
3382 // Arithmetic — all exact, no rounding
3383 // add :: (Decimal, Decimal) -> Decimal
3384 fields.insert("add".into(), Ty::function(
3385 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3386 // sub :: (Decimal, Decimal) -> Decimal
3387 fields.insert("sub".into(), Ty::function(
3388 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3389 // mul :: (Decimal, Decimal) -> Decimal — exponents add
3390 fields.insert("mul".into(), Ty::function(
3391 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3392 // Comparison — three-way: -1 / 0 / 1
3393 // compare :: (Decimal, Decimal) -> Int
3394 fields.insert("compare".into(), Ty::function(
3395 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), Ty::int()));
3396 // Predicates
3397 fields.insert("is_zero".into(), Ty::function(
3398 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3399 fields.insert("is_positive".into(), Ty::function(
3400 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3401 fields.insert("is_negative".into(), Ty::function(
3402 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3403 // Transformers
3404 // normalize :: Decimal -> Decimal — remove trailing zeros
3405 fields.insert("normalize".into(), Ty::function(
3406 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3407 // negate :: Decimal -> Decimal
3408 fields.insert("negate".into(), Ty::function(
3409 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3410 // abs :: Decimal -> Decimal
3411 fields.insert("abs".into(), Ty::function(
3412 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3413 // round_to :: (Decimal, Int, Str) -> Decimal
3414 // target_exp: the exponent to round to (e.g. -2 → 2 decimal places)
3415 // mode: "HalfUp" | "HalfDown" | "HalfEven" | "Down" | "Up" | "Ceiling" | "Floor"
3416 fields.insert("round_to".into(), Ty::function(
3417 vec![decimal_ty(), Ty::int(), Ty::str()],
3418 EffectSet::empty(), decimal_ty()));
3419 // to_str :: Decimal -> Str — decimal notation, e.g. "123.45"
3420 fields.insert("to_str".into(), Ty::function(
3421 vec![decimal_ty()], EffectSet::empty(), Ty::str()));
3422 // pow10 :: Int -> Int — 10^n; n must be in [0, 18]
3423 fields.insert("pow10".into(), Ty::function(
3424 vec![Ty::int()], EffectSet::empty(), Ty::int()));
3425 Some(Ty::Record(fields))
3426 }
3427 _ => None,
3428 }
3429}
3430
3431/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
3432pub fn module_for_import(reference: &str) -> Option<&'static str> {
3433 let suffix = reference.strip_prefix("std.")?;
3434 Some(match suffix {
3435 "io" => "io",
3436 "str" => "str",
3437 "int" => "int",
3438 "float" => "float",
3439 "list" => "list",
3440 "result" => "result",
3441 "option" => "option",
3442 "json" => "json",
3443 "flow" => "flow",
3444 "tuple" => "tuple",
3445 "time" => "time",
3446 "rand" => "rand",
3447 "random" => "random",
3448 "env" => "env",
3449 "bytes" => "bytes",
3450 "net" => "net",
3451 "tls" => "tls",
3452 "chat" => "chat",
3453 "math" => "math",
3454 "map" => "map",
3455 "set" => "set",
3456 "iter" => "iter",
3457 "crypto" => "crypto",
3458 "regex" => "regex",
3459 "parser" => "parser",
3460 "deque" => "deque",
3461 "kv" => "kv",
3462 "moe" => "moe",
3463 "sql" => "sql",
3464 "fs" => "fs",
3465 "process" => "process",
3466 "approval" => "approval",
3467 "datetime" => "datetime",
3468 "duration" => "duration",
3469 "log" => "log",
3470 "http" => "http",
3471 "toml" => "toml",
3472 "yaml" => "yaml",
3473 "dotenv" => "dotenv",
3474 "csv" => "csv",
3475 "test" => "test",
3476 "agent" => "agent",
3477 "cli" => "cli",
3478 "stream" => "stream",
3479 "conc" => "conc",
3480 "arrow" => "arrow",
3481 "df" => "df",
3482 "redis" => "redis",
3483 "decimal" => "decimal",
3484 "vcs" => "vcs",
3485 _ => return None,
3486 })
3487}
3488
3489/// Every module name `module_scope` resolves, in that function's match
3490/// order. Adding a match arm there means adding the name HERE too —
3491/// `lex docs --stdlib-index` renders docs/AGENT.md's generated stdlib
3492/// index from this list, and `lex doc-sync --check` in CI fails when the
3493/// docs drift from what this enumeration produces (#746).
3494pub const MODULE_NAMES: &[&str] = &[
3495 "io", "str", "int", "math", "float", "list", "bytes", "time", "rand", "random", "env", "net",
3496 "tls", "chat", "conc", "arrow", "df", "json", "result", "option", "tuple", "map", "set",
3497 "iter", "flow", "crypto", "deque", "log", "datetime", "duration", "approval", "process", "fs",
3498 "kv", "vcs", "sql", "redis", "parser", "cli", "regex", "http", "yaml", "dotenv", "csv", "test",
3499 "toml", "agent", "stream", "decimal", "moe",
3500];
3501
3502#[cfg(test)]
3503mod module_names_tests {
3504 use super::*;
3505 use crate::env::TypeEnv;
3506
3507 /// Every listed module must resolve to a non-empty record of
3508 /// builtins — a typo'd or removed name here would silently drop a
3509 /// module from the generated stdlib index.
3510 #[test]
3511 fn every_listed_module_resolves() {
3512 let env = TypeEnv::default();
3513 for name in MODULE_NAMES {
3514 match module_scope(name, &env) {
3515 Some(Ty::Record(fields)) => {
3516 assert!(!fields.is_empty(), "module `{name}` resolved to an empty record");
3517 }
3518 other => panic!("module `{name}` did not resolve to a record: {other:?}"),
3519 }
3520 }
3521 }
3522
3523 /// The list and `module_for_import` must agree: everything listed
3524 /// is reachable via `import "std.<name>"`.
3525 #[test]
3526 fn listed_modules_are_importable() {
3527 for name in MODULE_NAMES {
3528 assert!(
3529 module_for_import(&format!("std.{name}")).is_some(),
3530 "module `{name}` is listed but not importable as std.{name}"
3531 );
3532 }
3533 }
3534}