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
2372 // Walk-style queries [fs_walk]
2373 fields.insert("exists".into(), Ty::function(
2374 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2375 fields.insert("is_file".into(), Ty::function(
2376 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2377 fields.insert("is_dir".into(), Ty::function(
2378 vec![Ty::str()], EffectSet::singleton("fs_walk"), Ty::bool()));
2379 fields.insert("stat".into(), Ty::function(
2380 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2381 result_str(stat_t())));
2382 fields.insert("list_dir".into(), Ty::function(
2383 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2384 result_str(Ty::List(Box::new(Ty::str())))));
2385 fields.insert("walk".into(), Ty::function(
2386 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2387 result_str(Ty::List(Box::new(Ty::str())))));
2388 fields.insert("glob".into(), Ty::function(
2389 vec![Ty::str()], EffectSet::singleton("fs_walk"),
2390 result_str(Ty::List(Box::new(Ty::str())))));
2391 // Mutations [fs_write]
2392 fields.insert("mkdir_p".into(), Ty::function(
2393 vec![Ty::str()], EffectSet::singleton("fs_write"),
2394 result_str(Ty::Unit)));
2395 fields.insert("remove".into(), Ty::function(
2396 vec![Ty::str()], EffectSet::singleton("fs_write"),
2397 result_str(Ty::Unit)));
2398 fields.insert("copy".into(), Ty::function(
2399 vec![Ty::str(), Ty::str()],
2400 EffectSet {
2401 concrete: [crate::types::EffectKind::bare("fs_walk"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2402 var: None,
2403 },
2404 result_str(Ty::Unit)));
2405 Some(Ty::Record(fields))
2406 }
2407 "kv" => {
2408 // Embedded key-value store. The opaque `Kv` type is
2409 // backed by an Int handle into a process-wide registry.
2410 let kv_t = || Ty::Con("Kv".into(), vec![]);
2411 let mut fields = IndexMap::new();
2412 // open :: Str -> [kv, fs_write] Result[Kv, Str]
2413 fields.insert("open".into(), Ty::function(
2414 vec![Ty::str()],
2415 EffectSet {
2416 concrete: [crate::types::EffectKind::bare("kv"), crate::types::EffectKind::bare("fs_write")].into_iter().collect(),
2417 var: None,
2418 },
2419 Ty::Con("Result".into(), vec![kv_t(), Ty::str()])));
2420 // close :: Kv -> [kv] Unit
2421 fields.insert("close".into(), Ty::function(
2422 vec![kv_t()],
2423 EffectSet::singleton("kv"),
2424 Ty::Unit));
2425 // get :: Kv, Str -> [kv] Option[Bytes]
2426 fields.insert("get".into(), Ty::function(
2427 vec![kv_t(), Ty::str()],
2428 EffectSet::singleton("kv"),
2429 Ty::Con("Option".into(), vec![Ty::bytes()])));
2430 // put :: Kv, Str, Bytes -> [kv] Result[Unit, Str]
2431 fields.insert("put".into(), Ty::function(
2432 vec![kv_t(), Ty::str(), Ty::bytes()],
2433 EffectSet::singleton("kv"),
2434 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2435 // delete :: Kv, Str -> [kv] Result[Unit, Str]
2436 fields.insert("delete".into(), Ty::function(
2437 vec![kv_t(), Ty::str()],
2438 EffectSet::singleton("kv"),
2439 Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()])));
2440 // contains :: Kv, Str -> [kv] Bool
2441 fields.insert("contains".into(), Ty::function(
2442 vec![kv_t(), Ty::str()],
2443 EffectSet::singleton("kv"),
2444 Ty::bool()));
2445 // list_prefix :: Kv, Str -> [kv] List[Str]
2446 fields.insert("list_prefix".into(), Ty::function(
2447 vec![kv_t(), Ty::str()],
2448 EffectSet::singleton("kv"),
2449 Ty::List(Box::new(Ty::str()))));
2450 Some(Ty::Record(fields))
2451 }
2452 "moe" => {
2453 // MoE expert-store placement ops (lex-moe#25). Native
2454 // EffectHandler dispatch on the `moe` kind — see
2455 // lex-moe's crates/moe-policy/src/lib.rs `MoeHost` for
2456 // the Rust-side implementation these signatures describe.
2457 // A single process-wide tier cache backs every op, so
2458 // unlike `kv` there's no open/close handle to thread.
2459 let usage_entry_t = || {
2460 let mut f = IndexMap::new();
2461 f.insert("hash".into(), Ty::str());
2462 f.insert("size".into(), Ty::int());
2463 f.insert("temp".into(), Ty::float());
2464 f.insert("hits".into(), Ty::int());
2465 f.insert("loads".into(), Ty::int());
2466 Ty::Record(f)
2467 };
2468 let stats_t = || {
2469 let mut f = IndexMap::new();
2470 f.insert("hits".into(), Ty::int());
2471 f.insert("misses".into(), Ty::int());
2472 f.insert("prefetched".into(), Ty::int());
2473 f.insert("evictions".into(), Ty::int());
2474 f.insert("resident_bytes".into(), Ty::int());
2475 Ty::Record(f)
2476 };
2477 let mut fields = IndexMap::new();
2478 // pin(hash :: Str) -> [moe] Unit
2479 fields.insert("pin".into(), Ty::function(
2480 vec![Ty::str()],
2481 EffectSet::singleton("moe"),
2482 Ty::Unit));
2483 // unpin(hash :: Str) -> [moe] Unit
2484 fields.insert("unpin".into(), Ty::function(
2485 vec![Ty::str()],
2486 EffectSet::singleton("moe"),
2487 Ty::Unit));
2488 // prefetch_hint(hashes :: List[Str]) -> [moe] Int (loads spawned)
2489 fields.insert("prefetch_hint".into(), Ty::function(
2490 vec![Ty::List(Box::new(Ty::str()))],
2491 EffectSet::singleton("moe"),
2492 Ty::int()));
2493 // usage_snapshot() -> [moe] List[{hash, size, temp, hits, loads}]
2494 fields.insert("usage_snapshot".into(), Ty::function(
2495 vec![],
2496 EffectSet::singleton("moe"),
2497 Ty::List(Box::new(usage_entry_t()))));
2498 // stats() -> [moe] {hits, misses, prefetched, evictions, resident_bytes}
2499 fields.insert("stats".into(), Ty::function(
2500 vec![],
2501 EffectSet::singleton("moe"),
2502 stats_t()));
2503 Some(Ty::Record(fields))
2504 }
2505 "vcs" => {
2506 // Content-addressed blob store (#5 / M6.1b). `put_blob` returns the
2507 // lowercase hex SHA-256 of the content — the SAME id as
2508 // `crypto.sha256_str`, so blobs are interchangeable with loom's
2509 // SQLite-backed artifacts by id. `ref_set`/`ref_get` bind a name
2510 // (namespace + key) to a blob sha, e.g. namespace "loom/sprint-{id}",
2511 // key = node id (branch-per-sprint, #5). The on-disk layout matches
2512 // lex-store's blob CAS (<root>/blobs, <root>/blobrefs).
2513 //
2514 // fs_write/fs_read appear in the effect rows so `lex audit` sees the
2515 // disk touch; the store root is internal (~/.lex/store or
2516 // $LEX_STORE_ROOT) so no user-path allowlist applies.
2517 let mut fields = IndexMap::new();
2518 let vcs_w = || EffectSet {
2519 concrete: [crate::types::EffectKind::bare("vcs"),
2520 crate::types::EffectKind::bare("fs_write")]
2521 .into_iter().collect(),
2522 var: None,
2523 };
2524 let vcs_r = || EffectSet {
2525 concrete: [crate::types::EffectKind::bare("vcs"),
2526 crate::types::EffectKind::bare("fs_read")]
2527 .into_iter().collect(),
2528 var: None,
2529 };
2530 let res = |ok: Ty| Ty::Con("Result".into(), vec![ok, Ty::str()]);
2531
2532 // put_blob :: Str -> [vcs, fs_write] Result[Str, Str] (returns sha)
2533 fields.insert("put_blob".into(), Ty::function(
2534 vec![Ty::str()], vcs_w(), res(Ty::str())));
2535 // get_blob :: Str -> [vcs, fs_read] Result[Str, Str]
2536 fields.insert("get_blob".into(), Ty::function(
2537 vec![Ty::str()], vcs_r(), res(Ty::str())));
2538 // has_blob :: Str -> [vcs, fs_read] Bool
2539 fields.insert("has_blob".into(), Ty::function(
2540 vec![Ty::str()], vcs_r(), Ty::bool()));
2541 // ref_set :: Str, Str, Str -> [vcs, fs_write] Result[Unit, Str]
2542 fields.insert("ref_set".into(), Ty::function(
2543 vec![Ty::str(), Ty::str(), Ty::str()], vcs_w(), res(Ty::Unit)));
2544 // ref_get :: Str, Str -> [vcs, fs_read] Result[Str, Str] (key -> sha)
2545 fields.insert("ref_get".into(), Ty::function(
2546 vec![Ty::str(), Ty::str()], vcs_r(), res(Ty::str())));
2547 Some(Ty::Record(fields))
2548 }
2549 "sql" => {
2550 // Embedded SQL (SQLite via rusqlite). The opaque `Db` type is
2551 // backed by an Int handle into a process-wide registry (#362).
2552 //
2553 // Params use the typed `SqlParam` ADT (PStr|PInt|PFloat|PBool|PNull)
2554 // registered in env.rs, so callers don't have to stringify values.
2555 //
2556 // Transactions: sql.begin(db) → SqlTx; sql.commit/rollback(tx).
2557 // exec_tx / query_tx mirror exec / query but operate on a SqlTx.
2558 //
2559 // Row decoders: get_str / get_int / get_float / get_bool extract
2560 // typed columns from a row record by name.
2561 let db_t = || Ty::Con("Db".into(), vec![]);
2562 let tx_t = || Ty::Con("SqlTx".into(), vec![]);
2563 let sp_t = || Ty::Con("SqlParam".into(), vec![]);
2564 let params_t = || Ty::List(Box::new(sp_t()));
2565 let mut fields = IndexMap::new();
2566
2567 // SqlError = { message, code, detail } — populated with
2568 // SQLSTATE (Postgres) or symbolic SQLite error name (#380).
2569 let se_t = || Ty::Con("SqlError".into(), vec![]);
2570
2571 // open :: Str -> [sql, fs_write] Result[Db, SqlError]
2572 fields.insert("open".into(), Ty::function(
2573 vec![Ty::str()],
2574 EffectSet {
2575 concrete: [crate::types::EffectKind::bare("sql"),
2576 crate::types::EffectKind::bare("fs_write")]
2577 .into_iter().collect(),
2578 var: None,
2579 },
2580 Ty::Con("Result".into(), vec![db_t(), se_t()])));
2581
2582 // close :: Db -> [sql] Unit
2583 fields.insert("close".into(), Ty::function(
2584 vec![db_t()],
2585 EffectSet::singleton("sql"),
2586 Ty::Unit));
2587
2588 // exec :: Db, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2589 fields.insert("exec".into(), Ty::function(
2590 vec![db_t(), Ty::str(), params_t()],
2591 EffectSet::singleton("sql"),
2592 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2593
2594 // query[T] :: Db, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2595 fields.insert("query".into(), Ty::function(
2596 vec![db_t(), Ty::str(), params_t()],
2597 EffectSet::singleton("sql"),
2598 Ty::Con("Result".into(), vec![
2599 Ty::List(Box::new(Ty::Var(0))),
2600 se_t(),
2601 ])));
2602
2603 // query_iter[T] :: Db, Str, List[SqlParam] -> [sql] Result[Iter[T], SqlError]
2604 // Streaming variant of `query` (#379). Rows are pulled from
2605 // the server one at a time via an mpsc-backed cursor —
2606 // memory stays bounded regardless of result-set size.
2607 // Other ops on the same `Db` handle block until the cursor
2608 // is drained (single connection per Db).
2609 fields.insert("query_iter".into(), Ty::function(
2610 vec![db_t(), Ty::str(), params_t()],
2611 EffectSet::singleton("sql"),
2612 Ty::Con("Result".into(), vec![
2613 Ty::Con("Iter".into(), vec![Ty::Var(0)]),
2614 se_t(),
2615 ])));
2616
2617 // begin :: Db -> [sql] Result[SqlTx, SqlError]
2618 fields.insert("begin".into(), Ty::function(
2619 vec![db_t()],
2620 EffectSet::singleton("sql"),
2621 Ty::Con("Result".into(), vec![tx_t(), se_t()])));
2622
2623 // commit :: SqlTx -> [sql] Result[Unit, SqlError]
2624 fields.insert("commit".into(), Ty::function(
2625 vec![tx_t()],
2626 EffectSet::singleton("sql"),
2627 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2628
2629 // rollback :: SqlTx -> [sql] Result[Unit, SqlError]
2630 fields.insert("rollback".into(), Ty::function(
2631 vec![tx_t()],
2632 EffectSet::singleton("sql"),
2633 Ty::Con("Result".into(), vec![Ty::Unit, se_t()])));
2634
2635 // exec_tx :: SqlTx, Str, List[SqlParam] -> [sql] Result[Int, SqlError]
2636 fields.insert("exec_tx".into(), Ty::function(
2637 vec![tx_t(), Ty::str(), params_t()],
2638 EffectSet::singleton("sql"),
2639 Ty::Con("Result".into(), vec![Ty::int(), se_t()])));
2640
2641 // query_tx[T] :: SqlTx, Str, List[SqlParam] -> [sql] Result[List[T], SqlError]
2642 fields.insert("query_tx".into(), Ty::function(
2643 vec![tx_t(), Ty::str(), params_t()],
2644 EffectSet::singleton("sql"),
2645 Ty::Con("Result".into(), vec![
2646 Ty::List(Box::new(Ty::Var(0))),
2647 se_t(),
2648 ])));
2649
2650 // Row decoders: get_X[T] :: T, Str -> Option[X]
2651 // T is polymorphic so these work on any row record shape.
2652 fields.insert("get_str".into(), Ty::function(
2653 vec![Ty::Var(0), Ty::str()],
2654 EffectSet::empty(),
2655 Ty::Con("Option".into(), vec![Ty::str()])));
2656 fields.insert("get_int".into(), Ty::function(
2657 vec![Ty::Var(0), Ty::str()],
2658 EffectSet::empty(),
2659 Ty::Con("Option".into(), vec![Ty::int()])));
2660 fields.insert("get_float".into(), Ty::function(
2661 vec![Ty::Var(0), Ty::str()],
2662 EffectSet::empty(),
2663 Ty::Con("Option".into(), vec![Ty::float()])));
2664 fields.insert("get_bool".into(), Ty::function(
2665 vec![Ty::Var(0), Ty::str()],
2666 EffectSet::empty(),
2667 Ty::Con("Option".into(), vec![Ty::bool()])));
2668
2669 Some(Ty::Record(fields))
2670 }
2671 "redis" => {
2672 // Thin Redis client (#533). ConnRedis is an opaque handle backed by a
2673 // process-wide registry (same pattern as Db in std.sql). All ops carry
2674 // [net] — Redis is a TCP service; no separate [redis] effect.
2675 //
2676 // subscribe / psubscribe return Unit because they are blocking
2677 // infinite loops, consistent with net.serve_fn and ws.serve.
2678 //
2679 // subscribe/psubscribe open a *dedicated* connection internally —
2680 // Redis disallows non-Pub/Sub commands on a subscribed connection.
2681 let conn_t = || Ty::Con("ConnRedis".into(), vec![]);
2682 let mut fields = IndexMap::new();
2683
2684 // connect :: Str -> [net] Result[ConnRedis, Str]
2685 // url: "redis://host:6379" or "rediss://host:6380" (TLS)
2686 fields.insert("connect".into(), Ty::function(
2687 vec![Ty::str()],
2688 EffectSet::singleton("net"),
2689 Ty::Con("Result".into(), vec![conn_t(), Ty::str()])));
2690
2691 // close :: ConnRedis -> [net] Unit
2692 fields.insert("close".into(), Ty::function(
2693 vec![conn_t()],
2694 EffectSet::singleton("net"),
2695 Ty::Unit));
2696
2697 // ---- Key-value -----------------------------------------------
2698
2699 // get :: ConnRedis, Str -> [net] Option[Str]
2700 fields.insert("get".into(), Ty::function(
2701 vec![conn_t(), Ty::str()],
2702 EffectSet::singleton("net"),
2703 Ty::Con("Option".into(), vec![Ty::str()])));
2704
2705 // set :: ConnRedis, Str, Str -> [net] Unit
2706 fields.insert("set".into(), Ty::function(
2707 vec![conn_t(), Ty::str(), Ty::str()],
2708 EffectSet::singleton("net"),
2709 Ty::Unit));
2710
2711 // set_ex :: ConnRedis, Str, Str, Int -> [net] Unit
2712 fields.insert("set_ex".into(), Ty::function(
2713 vec![conn_t(), Ty::str(), Ty::str(), Ty::int()],
2714 EffectSet::singleton("net"),
2715 Ty::Unit));
2716
2717 // del :: ConnRedis, Str -> [net] Unit
2718 fields.insert("del".into(), Ty::function(
2719 vec![conn_t(), Ty::str()],
2720 EffectSet::singleton("net"),
2721 Ty::Unit));
2722
2723 // exists :: ConnRedis, Str -> [net] Bool
2724 fields.insert("exists".into(), Ty::function(
2725 vec![conn_t(), Ty::str()],
2726 EffectSet::singleton("net"),
2727 Ty::bool()));
2728
2729 // expire :: ConnRedis, Str, Int -> [net] Unit
2730 fields.insert("expire".into(), Ty::function(
2731 vec![conn_t(), Ty::str(), Ty::int()],
2732 EffectSet::singleton("net"),
2733 Ty::Unit));
2734
2735 // ---- Pub/Sub -------------------------------------------------
2736
2737 // publish :: ConnRedis, Str, Str -> [net] Int
2738 // Returns the number of subscribers that received the message.
2739 fields.insert("publish".into(), Ty::function(
2740 vec![conn_t(), Ty::str(), Ty::str()],
2741 EffectSet::singleton("net"),
2742 Ty::int()));
2743
2744 // subscribe :: ConnRedis, Str, (Str, Str ->[E] Unit) -> [net] Unit
2745 // Blocking loop; handler receives (channel, message) on each message.
2746 // Uses a dedicated connection — Redis disallows non-Pub/Sub commands
2747 // on a subscribed connection. Handler carries an open effect row so
2748 // callers can use io, net, sql, etc. inside the closure.
2749 let handler2 = Ty::function(
2750 vec![Ty::str(), Ty::str()],
2751 EffectSet::open_var(0),
2752 Ty::Unit);
2753 fields.insert("subscribe".into(), Ty::function(
2754 vec![conn_t(), Ty::str(), handler2],
2755 EffectSet::singleton("net"),
2756 Ty::Unit)); // Unit
2757
2758 // psubscribe :: ConnRedis, Str, (Str, Str, Str ->[E] Unit) -> [net] Unit
2759 // Pattern-subscribe; handler receives (pattern, channel, message).
2760 // Handler carries an open effect row (same rationale as subscribe).
2761 let handler3 = Ty::function(
2762 vec![Ty::str(), Ty::str(), Ty::str()],
2763 EffectSet::open_var(1),
2764 Ty::Unit);
2765 fields.insert("psubscribe".into(), Ty::function(
2766 vec![conn_t(), Ty::str(), handler3],
2767 EffectSet::singleton("net"),
2768 Ty::Unit)); // Unit
2769
2770 // ---- List ----------------------------------------------------
2771
2772 // lpush :: ConnRedis, Str, Str -> [net] Int
2773 fields.insert("lpush".into(), Ty::function(
2774 vec![conn_t(), Ty::str(), Ty::str()],
2775 EffectSet::singleton("net"),
2776 Ty::int()));
2777
2778 // rpush :: ConnRedis, Str, Str -> [net] Int
2779 fields.insert("rpush".into(), Ty::function(
2780 vec![conn_t(), Ty::str(), Ty::str()],
2781 EffectSet::singleton("net"),
2782 Ty::int()));
2783
2784 // brpop :: ConnRedis, Str, Int -> [net] Option[Str]
2785 // Blocking right-pop; returns None on timeout. timeout=0 blocks
2786 // indefinitely (the runtime does not treat this as a hung effect).
2787 fields.insert("brpop".into(), Ty::function(
2788 vec![conn_t(), Ty::str(), Ty::int()],
2789 EffectSet::singleton("net"),
2790 Ty::Con("Option".into(), vec![Ty::str()])));
2791
2792 // llen :: ConnRedis, Str -> [net] Int
2793 fields.insert("llen".into(), Ty::function(
2794 vec![conn_t(), Ty::str()],
2795 EffectSet::singleton("net"),
2796 Ty::int()));
2797
2798 // ---- Hash ----------------------------------------------------
2799
2800 // hset :: ConnRedis, Str, Str, Str -> [net] Unit
2801 fields.insert("hset".into(), Ty::function(
2802 vec![conn_t(), Ty::str(), Ty::str(), Ty::str()],
2803 EffectSet::singleton("net"),
2804 Ty::Unit));
2805
2806 // hget :: ConnRedis, Str, Str -> [net] Option[Str]
2807 fields.insert("hget".into(), Ty::function(
2808 vec![conn_t(), Ty::str(), Ty::str()],
2809 EffectSet::singleton("net"),
2810 Ty::Con("Option".into(), vec![Ty::str()])));
2811
2812 // hdel :: ConnRedis, Str, Str -> [net] Unit
2813 fields.insert("hdel".into(), Ty::function(
2814 vec![conn_t(), Ty::str(), Ty::str()],
2815 EffectSet::singleton("net"),
2816 Ty::Unit));
2817
2818 // hgetall :: ConnRedis, Str -> [net] List[(Str, Str)]
2819 fields.insert("hgetall".into(), Ty::function(
2820 vec![conn_t(), Ty::str()],
2821 EffectSet::singleton("net"),
2822 Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), Ty::str()])))));
2823
2824 Some(Ty::Record(fields))
2825 }
2826 "parser" => {
2827 // #217: structured parser combinators. Parser values are
2828 // tagged Records at runtime (`{ kind, ... }`), opaque at
2829 // the language level via `Ty::Con("Parser", [T])`.
2830 //
2831 // Surface:
2832 // - primitives: char, string, digit, alpha, whitespace, eof
2833 // - combinators: seq, alt, many, optional, map, and_then
2834 // - run :: Parser[T], Str -> Result[T, ParseErr]
2835 //
2836 // `map` and `and_then` were deferred from #217's v1 because
2837 // their closure arguments carried call-site identity that
2838 // broke the canonical-parsers acceptance criterion. With
2839 // closure body-hash equality landed in #222, that concern
2840 // is gone, and #221 wires them in. The interpreter for
2841 // `parser.run` has been moved to `lex-bytecode::parser_runtime`
2842 // so it can invoke closures from `Map` / `AndThen` nodes.
2843 let pt = |t: Ty| Ty::Con("Parser".into(), vec![t]);
2844 let parse_err = || {
2845 let mut fs = IndexMap::new();
2846 fs.insert("pos".into(), Ty::int());
2847 fs.insert("message".into(), Ty::str());
2848 Ty::Record(fs)
2849 };
2850 let mut fields = IndexMap::new();
2851 // char :: Str -> Parser[Str] (single-char Str literal)
2852 fields.insert("char".into(), Ty::function(
2853 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2854 // string :: Str -> Parser[Str]
2855 fields.insert("string".into(), Ty::function(
2856 vec![Ty::str()], EffectSet::empty(), pt(Ty::str())));
2857 // digit :: () -> Parser[Str]
2858 fields.insert("digit".into(), Ty::function(
2859 vec![], EffectSet::empty(), pt(Ty::str())));
2860 // alpha :: () -> Parser[Str]
2861 fields.insert("alpha".into(), Ty::function(
2862 vec![], EffectSet::empty(), pt(Ty::str())));
2863 // whitespace :: () -> Parser[Str]
2864 fields.insert("whitespace".into(), Ty::function(
2865 vec![], EffectSet::empty(), pt(Ty::str())));
2866 // eof :: () -> Parser[Unit]
2867 fields.insert("eof".into(), Ty::function(
2868 vec![], EffectSet::empty(), pt(Ty::Unit)));
2869 // seq :: Parser[A], Parser[B] -> Parser[(A, B)]
2870 fields.insert("seq".into(), Ty::function(
2871 vec![pt(Ty::Var(0)), pt(Ty::Var(1))],
2872 EffectSet::empty(),
2873 pt(Ty::Tuple(vec![Ty::Var(0), Ty::Var(1)]))));
2874 // alt :: Parser[T], Parser[T] -> Parser[T]
2875 // PEG-style ordered choice: the second alternative is
2876 // tried only if the first fails.
2877 fields.insert("alt".into(), Ty::function(
2878 vec![pt(Ty::Var(0)), pt(Ty::Var(0))],
2879 EffectSet::empty(),
2880 pt(Ty::Var(0))));
2881 // many :: Parser[T] -> Parser[List[T]]
2882 // Zero-or-more. Stops as soon as the inner parser fails
2883 // OR doesn't advance the position (avoids infinite loop
2884 // on empty matches).
2885 fields.insert("many".into(), Ty::function(
2886 vec![pt(Ty::Var(0))],
2887 EffectSet::empty(),
2888 pt(Ty::List(Box::new(Ty::Var(0))))));
2889 // optional :: Parser[T] -> Parser[Option[T]]
2890 fields.insert("optional".into(), Ty::function(
2891 vec![pt(Ty::Var(0))],
2892 EffectSet::empty(),
2893 pt(Ty::Con("Option".into(), vec![Ty::Var(0)]))));
2894 // map :: Parser[T], (T) -> [E] U -> [E] Parser[U]
2895 // The closure runs at parse time when the Parser is run.
2896 // Effect-polymorphic on the closure: any effect the
2897 // closure declares propagates to the surrounding `run`.
2898 fields.insert("map".into(), Ty::function(
2899 vec![
2900 pt(Ty::Var(0)),
2901 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(2), Ty::Var(1)),
2902 ],
2903 EffectSet::open_var(2),
2904 pt(Ty::Var(1))));
2905 // and_then :: Parser[T], (T) -> [E] Parser[U] -> [E] Parser[U]
2906 // Monadic bind: closure inspects the parsed value and
2907 // returns the next parser to run.
2908 fields.insert("and_then".into(), Ty::function(
2909 vec![
2910 pt(Ty::Var(0)),
2911 Ty::function(vec![Ty::Var(0)], EffectSet::open_var(3),
2912 pt(Ty::Var(1))),
2913 ],
2914 EffectSet::open_var(3),
2915 pt(Ty::Var(1))));
2916 // run :: Parser[T], Str -> Result[T, ParseErr]
2917 // ParseErr = { pos :: Int, message :: Str }
2918 fields.insert("run".into(), Ty::function(
2919 vec![pt(Ty::Var(0)), Ty::str()],
2920 EffectSet::empty(),
2921 Ty::Con("Result".into(), vec![Ty::Var(0), parse_err()])));
2922 Some(Ty::Record(fields))
2923 }
2924 "cli" => {
2925 // #224 Rubric port: argparse-equivalent for end-user
2926 // programs. Spec values are tagged `Json` records (opaque
2927 // to the language but inspectable). Construction via the
2928 // `flag` / `option` / `positional` / `spec` builders;
2929 // parse + introspection / help via the remaining ops.
2930 let json = || Ty::Con("Json".into(), vec![]);
2931 let opt_str = || Ty::Con("Option".into(), vec![Ty::str()]);
2932 let mut fields = IndexMap::new();
2933 // flag :: Str -> Option[Str] -> Str -> Json
2934 // long_name -> short -> help -> CliArg
2935 fields.insert("flag".into(), Ty::function(
2936 vec![Ty::str(), opt_str(), Ty::str()],
2937 EffectSet::empty(),
2938 json()));
2939 // option :: Str -> Option[Str] -> Str -> Option[Str] -> Json
2940 // long_name -> short -> help -> default -> CliArg
2941 fields.insert("option".into(), Ty::function(
2942 vec![Ty::str(), opt_str(), Ty::str(), opt_str()],
2943 EffectSet::empty(),
2944 json()));
2945 // positional :: Str -> Str -> Bool -> Json
2946 // name -> help -> required -> CliArg
2947 fields.insert("positional".into(), Ty::function(
2948 vec![Ty::str(), Ty::str(), Ty::bool()],
2949 EffectSet::empty(),
2950 json()));
2951 // spec :: Str -> Str -> List[Json] -> List[Json] -> Json
2952 // name -> help -> args -> subcommands -> CliSpec
2953 fields.insert("spec".into(), Ty::function(
2954 vec![Ty::str(), Ty::str(),
2955 Ty::List(Box::new(json())),
2956 Ty::List(Box::new(json()))],
2957 EffectSet::empty(),
2958 json()));
2959 // parse :: Json -> List[Str] -> Result[Json, Str]
2960 // spec -> argv -> Result[CliParsed, error]
2961 fields.insert("parse".into(), Ty::function(
2962 vec![json(), Ty::List(Box::new(Ty::str()))],
2963 EffectSet::empty(),
2964 Ty::Con("Result".into(), vec![json(), Ty::str()])));
2965 // envelope :: Bool -> Str -> T -> Json
2966 // ok -> command -> data -> ACLI-shaped envelope.
2967 // `data` is polymorphic so callers don't have to round-
2968 // trip through `json.parse` for trivial payloads.
2969 fields.insert("envelope".into(), Ty::function(
2970 vec![Ty::bool(), Ty::str(), Ty::Var(0)],
2971 EffectSet::empty(),
2972 json()));
2973 // describe :: Json -> Json — machine-readable spec dump
2974 fields.insert("describe".into(), Ty::function(
2975 vec![json()],
2976 EffectSet::empty(),
2977 json()));
2978 // help :: Json -> Str — human-readable help text
2979 fields.insert("help".into(), Ty::function(
2980 vec![json()],
2981 EffectSet::empty(),
2982 Ty::str()));
2983 Some(Ty::Record(fields))
2984 }
2985 "regex" => {
2986 // The compiled `Regex` is stored as a `Str` at runtime
2987 // (the pattern source) plus a process-wide cache of the
2988 // actual `regex::Regex`. So `Regex` is a nominal type at
2989 // the language level but its value is just the pattern.
2990 let regex_t = || Ty::Con("Regex".into(), vec![]);
2991 let match_t = || {
2992 let mut fs = IndexMap::new();
2993 fs.insert("text".into(), Ty::str());
2994 fs.insert("start".into(), Ty::int());
2995 fs.insert("end".into(), Ty::int());
2996 fs.insert("groups".into(), Ty::List(Box::new(Ty::str())));
2997 Ty::Record(fs)
2998 };
2999 let mut fields = IndexMap::new();
3000 // compile :: Str -> Result[Regex, Str]
3001 fields.insert("compile".into(), Ty::function(
3002 vec![Ty::str()], EffectSet::empty(),
3003 Ty::Con("Result".into(), vec![regex_t(), Ty::str()])));
3004 // is_match :: Regex, Str -> Bool
3005 fields.insert("is_match".into(), Ty::function(
3006 vec![regex_t(), Ty::str()], EffectSet::empty(), Ty::bool()));
3007 // is_match_str :: Str, Str -> Bool
3008 // Compiles the first argument as a pattern and matches against the second.
3009 // Returns false on invalid pattern instead of propagating an error.
3010 fields.insert("is_match_str".into(), Ty::function(
3011 vec![Ty::str(), Ty::str()], EffectSet::empty(), Ty::bool()));
3012 // find :: Regex, Str -> Option[Match]
3013 fields.insert("find".into(), Ty::function(
3014 vec![regex_t(), Ty::str()], EffectSet::empty(),
3015 Ty::Con("Option".into(), vec![match_t()])));
3016 // find_all :: Regex, Str -> List[Match]
3017 fields.insert("find_all".into(), Ty::function(
3018 vec![regex_t(), Ty::str()], EffectSet::empty(),
3019 Ty::List(Box::new(match_t()))));
3020 // replace :: Regex, Str, Str -> Str
3021 fields.insert("replace".into(), Ty::function(
3022 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
3023 // replace_all :: Regex, Str, Str -> Str
3024 fields.insert("replace_all".into(), Ty::function(
3025 vec![regex_t(), Ty::str(), Ty::str()], EffectSet::empty(), Ty::str()));
3026 // split :: Regex, Str -> List[Str]
3027 fields.insert("split".into(), Ty::function(
3028 vec![regex_t(), Ty::str()], EffectSet::empty(),
3029 Ty::List(Box::new(Ty::str()))));
3030 Some(Ty::Record(fields))
3031 }
3032 "http" => {
3033 // Rich HTTP client. `[net]` for the wire ops, pure for
3034 // the builders / decoders. `--allow-net-host` gates per
3035 // request. Multipart upload + streaming response bodies
3036 // are deferred to v1.5; the v1 surface covers the
3037 // common cases (auth, headers, query, timeouts, JSON /
3038 // text decoding).
3039 let req_t = || Ty::Con("HttpRequest".into(), vec![]);
3040 let resp_t = || Ty::Con("HttpResponse".into(), vec![]);
3041 let err_t = || Ty::Con("HttpError".into(), vec![]);
3042 let result_he = |t: Ty| Ty::Con("Result".into(), vec![t, err_t()]);
3043 let str_str_map = || Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]);
3044 let mut fields = IndexMap::new();
3045 // -- wire ops (effectful) --
3046 // send :: HttpRequest -> [net] Result[HttpResponse, HttpError]
3047 fields.insert("send".into(), Ty::function(
3048 vec![req_t()],
3049 EffectSet::singleton("net"),
3050 result_he(resp_t()),
3051 ));
3052 // get :: Str -> [net] Result[HttpResponse, HttpError]
3053 fields.insert("get".into(), Ty::function(
3054 vec![Ty::str()],
3055 EffectSet::singleton("net"),
3056 result_he(resp_t()),
3057 ));
3058 // post :: Str, Bytes, Str -> [net] Result[HttpResponse, HttpError]
3059 fields.insert("post".into(), Ty::function(
3060 vec![Ty::str(), Ty::bytes(), Ty::str()],
3061 EffectSet::singleton("net"),
3062 result_he(resp_t()),
3063 ));
3064 // -- pure builders (record transforms) --
3065 // with_header :: HttpRequest, Str, Str -> HttpRequest
3066 fields.insert("with_header".into(), Ty::function(
3067 vec![req_t(), Ty::str(), Ty::str()],
3068 EffectSet::empty(),
3069 req_t(),
3070 ));
3071 // with_auth :: HttpRequest, Str, Str -> HttpRequest
3072 // (Renders `<scheme> <token>` into the `Authorization`
3073 // header — `Bearer <jwt>`, `Basic <b64>`, etc.)
3074 fields.insert("with_auth".into(), Ty::function(
3075 vec![req_t(), Ty::str(), Ty::str()],
3076 EffectSet::empty(),
3077 req_t(),
3078 ));
3079 // with_query :: HttpRequest, Map[Str, Str] -> HttpRequest
3080 // (Appends a `?k=v&...` query string; values are URL-
3081 // encoded so `&` / `=` / spaces in values don't escape.)
3082 fields.insert("with_query".into(), Ty::function(
3083 vec![req_t(), str_str_map()],
3084 EffectSet::empty(),
3085 req_t(),
3086 ));
3087 // with_timeout_ms :: HttpRequest, Int -> HttpRequest
3088 fields.insert("with_timeout_ms".into(), Ty::function(
3089 vec![req_t(), Ty::int()],
3090 EffectSet::empty(),
3091 req_t(),
3092 ));
3093 // -- pure decoders --
3094 // json_body[T] :: HttpResponse -> Result[T, HttpError]
3095 // Polymorphic on the parsed shape, matching `json.parse`.
3096 fields.insert("json_body".into(), Ty::function(
3097 vec![resp_t()],
3098 EffectSet::empty(),
3099 result_he(Ty::Var(0)),
3100 ));
3101 // text_body :: HttpResponse -> Result[Str, HttpError]
3102 fields.insert("text_body".into(), Ty::function(
3103 vec![resp_t()],
3104 EffectSet::empty(),
3105 result_he(Ty::str()),
3106 ));
3107 // stream_lines :: Str, Map[Str, Str], Str -> [net] Result[Stream[Str], Str]
3108 // Streaming HTTP POST that yields the response body line-by-line
3109 // for SSE / NDJSON endpoints. Returns a lazy `Stream[Str]` (#683):
3110 // each `stream.next` pulls exactly one line off the socket as it
3111 // arrives, so an endpoint that holds the connection open and emits
3112 // events over time is consumed incrementally instead of blocking
3113 // until close. Connection errors at request time surface as
3114 // `Err(Str)`; a mid-stream read error / close ends the stream
3115 // (next `stream.next` returns `None`). Consume with `std.stream`
3116 // (`stream.next` / `stream.collect`), which carries `[stream]`.
3117 fields.insert("stream_lines".into(), Ty::function(
3118 vec![
3119 Ty::str(),
3120 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
3121 Ty::str(),
3122 ],
3123 EffectSet::singleton("net"),
3124 Ty::Con("Result".into(), vec![
3125 Ty::Con("Stream".into(), vec![Ty::str()]),
3126 Ty::str(),
3127 ]),
3128 ));
3129 Some(Ty::Record(fields))
3130 }
3131 "yaml" => {
3132 // YAML config parser. Same shape as `std.toml`: parse
3133 // is polymorphic, output Value layout matches std.json
3134 // (Str/Int/Float/Bool/List/Record). Anchors and tags
3135 // are flattened by serde_yaml's deserializer.
3136 let mut fields = IndexMap::new();
3137 fields.insert("parse".into(), Ty::function(
3138 vec![Ty::str()], EffectSet::empty(),
3139 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3140 ));
3141 // Tactical fix for #168 — caller-supplied required-field
3142 // list. See std.json's parse_strict for context.
3143 fields.insert("parse_strict".into(), Ty::function(
3144 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3145 EffectSet::empty(),
3146 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3147 ));
3148 fields.insert("stringify".into(), Ty::function(
3149 vec![Ty::Var(0)], EffectSet::empty(),
3150 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3151 ));
3152 Some(Ty::Record(fields))
3153 }
3154 "dotenv" => {
3155 // .env-style files. parse :: Str -> Result[Map[Str,Str], Str].
3156 // Returns a map (not a polymorphic record) because
3157 // dotenv files don't carry shape — every value is a
3158 // string and keys aren't statically known.
3159 let mut fields = IndexMap::new();
3160 fields.insert("parse".into(), Ty::function(
3161 vec![Ty::str()], EffectSet::empty(),
3162 Ty::Con("Result".into(), vec![
3163 Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]),
3164 Ty::str(),
3165 ]),
3166 ));
3167 Some(Ty::Record(fields))
3168 }
3169 "csv" => {
3170 // CSV rows-as-lists. parse :: Str -> Result[List[List[Str]], Str].
3171 // Header awareness is left to the caller — row 0 is
3172 // whatever the file has. A `parse_with_headers` that
3173 // returns List[Map[Str,Str]] is a natural follow-up.
3174 let row_ty = Ty::List(Box::new(Ty::str()));
3175 let rows_ty = Ty::List(Box::new(row_ty.clone()));
3176 let mut fields = IndexMap::new();
3177 fields.insert("parse".into(), Ty::function(
3178 vec![Ty::str()], EffectSet::empty(),
3179 Ty::Con("Result".into(), vec![rows_ty.clone(), Ty::str()]),
3180 ));
3181 fields.insert("stringify".into(), Ty::function(
3182 vec![rows_ty], EffectSet::empty(),
3183 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3184 ));
3185 Some(Ty::Record(fields))
3186 }
3187 "test" => {
3188 // Tiny assertion library (#proposed-stdlib). Each helper
3189 // returns Result[Unit, Str] so a test is itself a fn
3190 // returning Result. Callers compose suites in user code
3191 // (a List of (name, () -> Result[Unit, Str]) pairs +
3192 // list.fold to accumulate verdicts). Property generators
3193 // and a Rust-side Suite type are deferred to v2.
3194 let mut fields = IndexMap::new();
3195 // assert_eq[a, b] :: T -> T -> Result[Unit, Str]
3196 // (T constrained equal by unification on the two args)
3197 let unit_result = || Ty::Con("Result".into(), vec![Ty::Unit, Ty::str()]);
3198 fields.insert("assert_eq".into(), Ty::function(
3199 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3200 ));
3201 fields.insert("assert_ne".into(), Ty::function(
3202 vec![Ty::Var(0), Ty::Var(0)], EffectSet::empty(), unit_result(),
3203 ));
3204 fields.insert("assert_true".into(), Ty::function(
3205 vec![Ty::bool()], EffectSet::empty(), unit_result(),
3206 ));
3207 fields.insert("assert_false".into(), Ty::function(
3208 vec![Ty::bool()], EffectSet::empty(), unit_result(),
3209 ));
3210 Some(Ty::Record(fields))
3211 }
3212 "toml" => {
3213 // TOML config parser. Mirrors `std.json`'s shape: parse
3214 // is polymorphic so callers annotate the expected
3215 // record / list / scalar shape and the type checker
3216 // unifies. The parsed TOML maps to the same Lex Value
3217 // shape as JSON does:
3218 //
3219 // TOML String → Value::Str
3220 // TOML Integer → Value::Int
3221 // TOML Float → Value::Float
3222 // TOML Boolean → Value::Bool
3223 // TOML Array → Value::List
3224 // TOML Table → Value::Record
3225 // TOML Datetime → Value::Str (RFC 3339, lossless)
3226 //
3227 // The Datetime → Str fallback is the one info-losing
3228 // step; callers who want a real `Instant` can pipe the
3229 // string through `datetime.parse_iso`.
3230 let mut fields = IndexMap::new();
3231 // parse :: Str -> Result[T, Str]
3232 fields.insert("parse".into(), Ty::function(
3233 vec![Ty::str()], EffectSet::empty(),
3234 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3235 ));
3236 // parse_strict :: (Str, List[Str]) -> Result[T, Str]
3237 // Tactical fix for #168 — caller passes the field
3238 // names T requires; runtime returns Err if any are
3239 // missing from the parsed table instead of letting
3240 // field access panic later.
3241 fields.insert("parse_strict".into(), Ty::function(
3242 vec![Ty::str(), Ty::List(Box::new(Ty::str()))],
3243 EffectSet::empty(),
3244 Ty::Con("Result".into(), vec![Ty::Var(0), Ty::str()]),
3245 ));
3246 // stringify :: T -> Result[Str, Str]
3247 // Returns Result (not Str) because not every Lex Value
3248 // has a TOML representation — top-level scalars,
3249 // closures, mixed-key maps etc. surface as Err rather
3250 // than panic.
3251 fields.insert("stringify".into(), Ty::function(
3252 vec![Ty::Var(0)], EffectSet::empty(),
3253 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3254 ));
3255 Some(Ty::Record(fields))
3256 }
3257 // `std.agent` (#184) — runtime primitives whose effects
3258 // separate (a) which LLM surface (`llm_local` vs
3259 // `llm_cloud`), (b) which peer protocol (`a2a`), and
3260 // (c) which tool boundary (`mcp`). The wire formats land
3261 // in downstream crates (`soft-agent`, `soft-a2a`) and
3262 // in #185 for MCP; what's typed here is the boundary
3263 // alone — agent code can be type-checked as
3264 // `[llm_local, a2a]` and will fail if it tries to reach
3265 // `[llm_cloud]` even before the wire layer is finished.
3266 "agent" => {
3267 let mut fields = IndexMap::new();
3268 // local_complete :: Str -> [llm_local] Result[Str, Str]
3269 fields.insert("local_complete".into(), Ty::function(
3270 vec![Ty::str()],
3271 EffectSet::singleton("llm_local"),
3272 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3273 ));
3274 // cloud_complete :: Str -> [llm_cloud] Result[Str, Str]
3275 fields.insert("cloud_complete".into(), Ty::function(
3276 vec![Ty::str()],
3277 EffectSet::singleton("llm_cloud"),
3278 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3279 ));
3280 // send_a2a :: (Str, Str) -> [a2a] Result[Str, Str]
3281 // peer payload reply
3282 fields.insert("send_a2a".into(), Ty::function(
3283 vec![Ty::str(), Ty::str()],
3284 EffectSet::singleton("a2a"),
3285 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3286 ));
3287 // call_mcp :: (Str, Str, Str) -> [mcp] Result[Str, Str]
3288 // server tool args_json result_json
3289 fields.insert("call_mcp".into(), Ty::function(
3290 vec![Ty::str(), Ty::str(), Ty::str()],
3291 EffectSet::singleton("mcp"),
3292 Ty::Con("Result".into(), vec![Ty::str(), Ty::str()]),
3293 ));
3294 // cloud_stream :: Str -> [llm_cloud] Result[Stream[Str], Str]
3295 // (#305 slice 3). Streaming counterpart to cloud_complete.
3296 // The result is `Result[Stream[Str], Str]` rather than a
3297 // bare Stream so transport errors surface synchronously
3298 // at handshake time; per-chunk errors collapse the
3299 // stream to early termination.
3300 fields.insert("cloud_stream".into(), Ty::function(
3301 vec![Ty::str()],
3302 EffectSet::singleton("llm_cloud"),
3303 Ty::Con("Result".into(), vec![
3304 Ty::Con("Stream".into(), vec![Ty::str()]),
3305 Ty::str(),
3306 ]),
3307 ));
3308 Some(Ty::Record(fields))
3309 }
3310 "stream" => {
3311 // #305 slice 3: opaque consumer-side operations on
3312 // `Stream[T]`. Producers live elsewhere (`agent.cloud_stream`
3313 // for now); future producers (`http.get_stream`, etc.)
3314 // will register the same Stream[T] surface.
3315 let mut fields = IndexMap::new();
3316 // next :: Stream[T] -> [stream] Option[T]
3317 // One pull. `None` signals end-of-stream (consumed by
3318 // the producer's lazy generator).
3319 fields.insert("next".into(), Ty::function(
3320 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3321 EffectSet::singleton("stream"),
3322 Ty::Con("Option".into(), vec![Ty::Var(0)]),
3323 ));
3324 // collect :: Stream[T] -> [stream] List[T]
3325 // Drain to a list. Eager; blocks until the producer
3326 // signals end-of-stream.
3327 fields.insert("collect".into(), Ty::function(
3328 vec![Ty::Con("Stream".into(), vec![Ty::Var(0)])],
3329 EffectSet::singleton("stream"),
3330 Ty::List(Box::new(Ty::Var(0))),
3331 ));
3332 Some(Ty::Record(fields))
3333 }
3334 // -- std.decimal (#574): exact decimal arithmetic with explicit rounding.
3335 // `Decimal = { coefficient :: Int, exponent :: Int }` where the value
3336 // is `coefficient × 10^exponent`. All arithmetic is exact (no IEEE 754
3337 // approximation); rounding only happens at `round_to`, which demands an
3338 // explicit mode string ("HalfUp" | "HalfDown" | "HalfEven" |
3339 // "Down" | "Up" | "Ceiling" | "Floor").
3340 "decimal" => {
3341 // Local helper: the Decimal record type.
3342 let decimal_ty = || {
3343 let mut f = IndexMap::new();
3344 f.insert("coefficient".into(), Ty::int());
3345 f.insert("exponent".into(), Ty::int());
3346 Ty::Record(f)
3347 };
3348 let mut fields = IndexMap::new();
3349 // Constructors
3350 // decimal :: (Int, Int) -> Decimal — coefficient, exponent
3351 fields.insert("decimal".into(), Ty::function(
3352 vec![Ty::int(), Ty::int()], EffectSet::empty(), decimal_ty()));
3353 // zero :: () -> Decimal — 0 × 10^0
3354 fields.insert("zero".into(), Ty::function(
3355 vec![], EffectSet::empty(), decimal_ty()));
3356 // one :: () -> Decimal — 1 × 10^0
3357 fields.insert("one".into(), Ty::function(
3358 vec![], EffectSet::empty(), decimal_ty()));
3359 // from_int :: Int -> Decimal — lift integer, exponent=0
3360 fields.insert("from_int".into(), Ty::function(
3361 vec![Ty::int()], EffectSet::empty(), decimal_ty()));
3362 // Arithmetic — all exact, no rounding
3363 // add :: (Decimal, Decimal) -> Decimal
3364 fields.insert("add".into(), Ty::function(
3365 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3366 // sub :: (Decimal, Decimal) -> Decimal
3367 fields.insert("sub".into(), Ty::function(
3368 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3369 // mul :: (Decimal, Decimal) -> Decimal — exponents add
3370 fields.insert("mul".into(), Ty::function(
3371 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), decimal_ty()));
3372 // Comparison — three-way: -1 / 0 / 1
3373 // compare :: (Decimal, Decimal) -> Int
3374 fields.insert("compare".into(), Ty::function(
3375 vec![decimal_ty(), decimal_ty()], EffectSet::empty(), Ty::int()));
3376 // Predicates
3377 fields.insert("is_zero".into(), Ty::function(
3378 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3379 fields.insert("is_positive".into(), Ty::function(
3380 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3381 fields.insert("is_negative".into(), Ty::function(
3382 vec![decimal_ty()], EffectSet::empty(), Ty::bool()));
3383 // Transformers
3384 // normalize :: Decimal -> Decimal — remove trailing zeros
3385 fields.insert("normalize".into(), Ty::function(
3386 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3387 // negate :: Decimal -> Decimal
3388 fields.insert("negate".into(), Ty::function(
3389 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3390 // abs :: Decimal -> Decimal
3391 fields.insert("abs".into(), Ty::function(
3392 vec![decimal_ty()], EffectSet::empty(), decimal_ty()));
3393 // round_to :: (Decimal, Int, Str) -> Decimal
3394 // target_exp: the exponent to round to (e.g. -2 → 2 decimal places)
3395 // mode: "HalfUp" | "HalfDown" | "HalfEven" | "Down" | "Up" | "Ceiling" | "Floor"
3396 fields.insert("round_to".into(), Ty::function(
3397 vec![decimal_ty(), Ty::int(), Ty::str()],
3398 EffectSet::empty(), decimal_ty()));
3399 // to_str :: Decimal -> Str — decimal notation, e.g. "123.45"
3400 fields.insert("to_str".into(), Ty::function(
3401 vec![decimal_ty()], EffectSet::empty(), Ty::str()));
3402 // pow10 :: Int -> Int — 10^n; n must be in [0, 18]
3403 fields.insert("pow10".into(), Ty::function(
3404 vec![Ty::int()], EffectSet::empty(), Ty::int()));
3405 Some(Ty::Record(fields))
3406 }
3407 _ => None,
3408 }
3409}
3410
3411/// Resolve `import "std.foo" as alias` to a module name (e.g. "io").
3412pub fn module_for_import(reference: &str) -> Option<&'static str> {
3413 let suffix = reference.strip_prefix("std.")?;
3414 Some(match suffix {
3415 "io" => "io",
3416 "str" => "str",
3417 "int" => "int",
3418 "float" => "float",
3419 "list" => "list",
3420 "result" => "result",
3421 "option" => "option",
3422 "json" => "json",
3423 "flow" => "flow",
3424 "tuple" => "tuple",
3425 "time" => "time",
3426 "rand" => "rand",
3427 "random" => "random",
3428 "env" => "env",
3429 "bytes" => "bytes",
3430 "net" => "net",
3431 "tls" => "tls",
3432 "chat" => "chat",
3433 "math" => "math",
3434 "map" => "map",
3435 "set" => "set",
3436 "iter" => "iter",
3437 "crypto" => "crypto",
3438 "regex" => "regex",
3439 "parser" => "parser",
3440 "deque" => "deque",
3441 "kv" => "kv",
3442 "moe" => "moe",
3443 "sql" => "sql",
3444 "fs" => "fs",
3445 "process" => "process",
3446 "approval" => "approval",
3447 "datetime" => "datetime",
3448 "duration" => "duration",
3449 "log" => "log",
3450 "http" => "http",
3451 "toml" => "toml",
3452 "yaml" => "yaml",
3453 "dotenv" => "dotenv",
3454 "csv" => "csv",
3455 "test" => "test",
3456 "agent" => "agent",
3457 "cli" => "cli",
3458 "stream" => "stream",
3459 "conc" => "conc",
3460 "arrow" => "arrow",
3461 "df" => "df",
3462 "redis" => "redis",
3463 "decimal" => "decimal",
3464 "vcs" => "vcs",
3465 _ => return None,
3466 })
3467}
3468
3469/// Every module name `module_scope` resolves, in that function's match
3470/// order. Adding a match arm there means adding the name HERE too —
3471/// `lex docs --stdlib-index` renders docs/AGENT.md's generated stdlib
3472/// index from this list, and `lex doc-sync --check` in CI fails when the
3473/// docs drift from what this enumeration produces (#746).
3474pub const MODULE_NAMES: &[&str] = &[
3475 "io", "str", "int", "math", "float", "list", "bytes", "time", "rand", "random", "env", "net",
3476 "tls", "chat", "conc", "arrow", "df", "json", "result", "option", "tuple", "map", "set",
3477 "iter", "flow", "crypto", "deque", "log", "datetime", "duration", "approval", "process", "fs",
3478 "kv", "vcs", "sql", "redis", "parser", "cli", "regex", "http", "yaml", "dotenv", "csv", "test",
3479 "toml", "agent", "stream", "decimal", "moe",
3480];
3481
3482#[cfg(test)]
3483mod module_names_tests {
3484 use super::*;
3485 use crate::env::TypeEnv;
3486
3487 /// Every listed module must resolve to a non-empty record of
3488 /// builtins — a typo'd or removed name here would silently drop a
3489 /// module from the generated stdlib index.
3490 #[test]
3491 fn every_listed_module_resolves() {
3492 let env = TypeEnv::default();
3493 for name in MODULE_NAMES {
3494 match module_scope(name, &env) {
3495 Some(Ty::Record(fields)) => {
3496 assert!(!fields.is_empty(), "module `{name}` resolved to an empty record");
3497 }
3498 other => panic!("module `{name}` did not resolve to a record: {other:?}"),
3499 }
3500 }
3501 }
3502
3503 /// The list and `module_for_import` must agree: everything listed
3504 /// is reachable via `import "std.<name>"`.
3505 #[test]
3506 fn listed_modules_are_importable() {
3507 for name in MODULE_NAMES {
3508 assert!(
3509 module_for_import(&format!("std.{name}")).is_some(),
3510 "module `{name}` is listed but not importable as std.{name}"
3511 );
3512 }
3513 }
3514}