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