beck_core/prelude.rs
1//! The standard library of the walking skeleton.
2//!
3//! Small on purpose. §3.2's promise is that "effect polymorphism is what keeps one standard
4//! library" — `map : (list[a], (a -> b ! e)) -> list[b] ! e`. Phase 2 has effect rows, so that
5//! signature is now written as written: `map_list` is polymorphic in what its function argument
6//! does, and mapping an effectful function over a list is effectful *in exactly that way*. One
7//! library, one definition per operation, usable from any tier the placement solver allows.
8//!
9//! The rows here are the source of truth for inference. [`Prim::effects`] is the same information
10//! for the atoms a primitive performs *itself*, and a test holds the two in agreement.
11//!
12//! Everything here is a [`Prim`], which means the evaluator implements it and the eventual
13//! Cranelift/LLVM backends implement it — never a Beck-source shim that would have to be compiled
14//! twice.
15
16use std::collections::BTreeMap;
17use std::sync::Arc;
18
19use crate::core::Prim;
20use crate::ty::{Effect, MethodSig, Row, RowVarId, Scheme, TraitSig, Ty, TyDecl, Variant};
21
22/// A fresh type variable id for a scheme. Scheme variables are numbered from a private range that
23/// never collides with the inference variables `Subst` mints, because `instantiate` replaces them.
24const A: u32 = 1_000_000;
25const B: u32 = 1_000_001;
26const C: u32 = 1_000_002;
27
28/// Row-variable ids for the schemes below, in their own range for the same reason `A`/`B`/`C` are:
29/// `instantiate` replaces them, so they can never collide with an inference variable.
30const E: RowVarId = 2_000_000;
31
32fn v(id: u32) -> Ty {
33 Ty::Var(id)
34}
35
36fn poly(vars: &[u32], ty: Ty) -> Scheme {
37 Scheme {
38 vars: vars.to_vec(),
39 row_vars: Vec::new(),
40 params: Vec::new(),
41 ty,
42 }
43}
44
45/// A scheme polymorphic in both dimensions — §3.2's `(list[a], (a -> b ! e)) -> list[b] ! e`.
46fn poly_eff(vars: &[u32], row_vars: &[RowVarId], ty: Ty) -> Scheme {
47 Scheme {
48 vars: vars.to_vec(),
49 row_vars: row_vars.to_vec(),
50 params: Vec::new(),
51 ty,
52 }
53}
54
55/// A pure function type.
56fn fun(params: Vec<Ty>, ret: Ty) -> Ty {
57 Ty::fun(params, ret)
58}
59
60/// A function type with an effect row.
61fn fun_eff(params: Vec<Ty>, ret: Ty, row: Row) -> Ty {
62 Ty::fun_eff(params, ret, row)
63}
64
65/// Every primitive's name and type.
66pub fn prims() -> Vec<(&'static str, Prim, Scheme)> {
67 let int = Ty::int();
68 let bool_ = Ty::bool_();
69 let str_ = Ty::str_();
70 let float = Ty::con(Ty::FLOAT);
71 let html = Ty::html();
72 let attr = Ty::con(Ty::ATTR);
73
74 vec![
75 // `+` is resolved bidirectionally in `check` so that it can also concatenate strings
76 // without introducing a numeric type class; the scheme here is its Int form.
77 (
78 "+",
79 Prim::Add,
80 Scheme::mono(fun(vec![int.clone(), int.clone()], int.clone())),
81 ),
82 (
83 "-",
84 Prim::Sub,
85 Scheme::mono(fun(vec![int.clone(), int.clone()], int.clone())),
86 ),
87 (
88 "*",
89 Prim::Mul,
90 Scheme::mono(fun(vec![int.clone(), int.clone()], int.clone())),
91 ),
92 (
93 "/",
94 Prim::Div,
95 Scheme::mono(fun(vec![int.clone(), int.clone()], int.clone())),
96 ),
97 (
98 "%",
99 Prim::Rem,
100 Scheme::mono(fun(vec![int.clone(), int.clone()], int.clone())),
101 ),
102 // The reals. `abs` is written for both tiers in SICP and is resolved from its operand in
103 // `check`, exactly as `+` is; the scheme here is its `Int` form, which is what a reference
104 // to it *as a value* gets (`docs/32` §32.3).
105 (
106 "abs",
107 Prim::Abs,
108 Scheme::mono(fun(vec![int.clone()], int.clone())),
109 ),
110 (
111 "sqrt",
112 Prim::Sqrt,
113 Scheme::mono(fun(vec![float.clone()], float.clone())),
114 ),
115 // The two trigonometric functions, and the conversion back down from a real.
116 //
117 // `docs/32` built the reals with `sqrt` and nothing else, and three phases of programs
118 // never asked for more — until a benchmark did. `Math.sin`, `Math.cos` and a `(int)` cast
119 // are what Are We Fast Yet's collision detector is written in, and a numeric tower with a
120 // square root and no sine is a gap rather than a decision (`docs/60` §60.2).
121 //
122 // All three are the host's, in `lib/README.md`'s sense: `sin` is somebody else's polynomial
123 // and truncation is the format's own rule about which way a real falls. Composition would
124 // be a slower, less correct copy — and unlike `money.beck`'s arithmetic there is nothing
125 // here that Beck *should* be expressing.
126 (
127 "sin",
128 Prim::Sin,
129 Scheme::mono(fun(vec![float.clone()], float.clone())),
130 ),
131 (
132 "cos",
133 Prim::Cos,
134 Scheme::mono(fun(vec![float.clone()], float.clone())),
135 ),
136 // Toward zero, which is what every language with this conversion means by it and what
137 // IEEE 754 calls `roundTowardZero`. Saturating rather than wrapping at the ends of `Int`,
138 // because a wrap would turn a large real into a small integer of the wrong sign.
139 (
140 "trunc",
141 Prim::Trunc,
142 Scheme::mono(fun(vec![float.clone()], int.clone())),
143 ),
144 (
145 "float",
146 Prim::ToFloat,
147 Scheme::mono(fun(vec![int.clone()], float.clone())),
148 ),
149 (
150 "negate",
151 Prim::Neg,
152 Scheme::mono(fun(vec![int.clone()], int.clone())),
153 ),
154 (
155 "==",
156 Prim::Eq,
157 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
158 ),
159 (
160 "!=",
161 Prim::Ne,
162 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
163 ),
164 (
165 "<",
166 Prim::Lt,
167 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
168 ),
169 (
170 "<=",
171 Prim::Le,
172 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
173 ),
174 (
175 ">",
176 Prim::Gt,
177 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
178 ),
179 (
180 ">=",
181 Prim::Ge,
182 poly(&[A], fun(vec![v(A), v(A)], bool_.clone())),
183 ),
184 (
185 "and",
186 Prim::And,
187 Scheme::mono(fun(vec![bool_.clone(), bool_.clone()], bool_.clone())),
188 ),
189 (
190 "or",
191 Prim::Or,
192 Scheme::mono(fun(vec![bool_.clone(), bool_.clone()], bool_.clone())),
193 ),
194 (
195 "not",
196 Prim::Not,
197 Scheme::mono(fun(vec![bool_.clone()], bool_.clone())),
198 ),
199 (
200 "str",
201 Prim::ToStr,
202 poly(&[A], fun(vec![v(A)], str_.clone())),
203 ),
204 (
205 "str_trim",
206 Prim::StrTrim,
207 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
208 ),
209 // The canonical fallible operation, and the reason it is here rather than in the standard
210 // library Wave 2 will write: `corpus/29-fallible.beck` needs one thing that can genuinely
211 // fail on its input, and a parse is that thing in every language.
212 (
213 "str_to_int",
214 Prim::StrToInt,
215 Scheme::mono(fun(vec![str_.clone()], Ty::option(Ty::int()))),
216 ),
217 // ------------------------------------------------------------------------ strings
218 //
219 // Wave 2's string half ([`docs/08`](../../../../../docs/08-roadmap.md) §8.5.4). Every one of
220 // these is a primitive rather than a definition written in Beck, and the reason is the
221 // same in each case: a string is where the host has to be asked. `str_upper` is a Unicode
222 // table, `str_split` is an allocation strategy, and writing either of them over a
223 // `list[Str]` of characters in Beck would be a slower, less correct copy of what the host
224 // already has. Where there *is* something to express — a `Decimal`, a `Json` document —
225 // Wave 2 writes it in Beck instead, which is the distinction §1.1 claims to be able to
226 // make.
227 //
228 // Positions are counted in **characters** — Unicode scalar values — and `str_len`,
229 // `str_slice` and `str_index_of` are one unit or they are a trap;
230 // `stdlib.rs::string_positions_are_characters_everywhere_or_nowhere` is where that is held.
231 //
232 // `str_slice(s, start, count)` takes a **count**, not an end index. Worth stating because
233 // the signature cannot: a primitive's parameters have no names in the generated reference,
234 // so `(Str, Int, Int) -> Str` reads either way and the first caller to pass a non-zero
235 // start with a real count got it wrong (`docs/55` §55.5).
236 //
237 // Both are clamped rather than refused: a slice past the end is the empty string, not a
238 // failure. That is a decision and not an oversight — a slice is not a parse, and `raises`
239 // is for a program's own vocabulary rather than for the standard library's arithmetic
240 // ([`45`](../../../../../docs/45-error-rows-report.md)).
241 (
242 "str_len",
243 Prim::StrLen,
244 Scheme::mono(fun(vec![str_.clone()], int.clone())),
245 ),
246 (
247 "str_slice",
248 Prim::StrSlice,
249 Scheme::mono(fun(
250 vec![str_.clone(), int.clone(), int.clone()],
251 str_.clone(),
252 )),
253 ),
254 (
255 "str_split",
256 Prim::StrSplit,
257 Scheme::mono(fun(
258 vec![str_.clone(), str_.clone()],
259 Ty::list(str_.clone()),
260 )),
261 ),
262 (
263 "str_join",
264 Prim::StrJoin,
265 Scheme::mono(fun(
266 vec![Ty::list(str_.clone()), str_.clone()],
267 str_.clone(),
268 )),
269 ),
270 (
271 "str_contains",
272 Prim::StrContains,
273 Scheme::mono(fun(vec![str_.clone(), str_.clone()], bool_.clone())),
274 ),
275 (
276 "str_starts_with",
277 Prim::StrStartsWith,
278 Scheme::mono(fun(vec![str_.clone(), str_.clone()], bool_.clone())),
279 ),
280 (
281 "str_ends_with",
282 Prim::StrEndsWith,
283 Scheme::mono(fun(vec![str_.clone(), str_.clone()], bool_.clone())),
284 ),
285 (
286 "str_upper",
287 Prim::StrUpper,
288 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
289 ),
290 (
291 "str_lower",
292 Prim::StrLower,
293 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
294 ),
295 (
296 "str_replace",
297 Prim::StrReplace,
298 Scheme::mono(fun(
299 vec![str_.clone(), str_.clone(), str_.clone()],
300 str_.clone(),
301 )),
302 ),
303 (
304 "str_index_of",
305 Prim::StrIndexOf,
306 Scheme::mono(fun(
307 vec![str_.clone(), str_.clone()],
308 Ty::option(int.clone()),
309 )),
310 ),
311 (
312 "str_repeat",
313 Prim::StrRepeat,
314 Scheme::mono(fun(vec![str_.clone(), int.clone()], str_.clone())),
315 ),
316 (
317 "str_chars",
318 Prim::StrChars,
319 Scheme::mono(fun(vec![str_.clone()], Ty::list(str_.clone()))),
320 ),
321 // ------------------------------------------------------------------------ collections
322 //
323 // The higher-order ones are row-polymorphic in the argument's effects — §3.2's
324 // `map : (list[a], (a -> b ! e)) -> list[b] ! e`, which
325 // [`33`](../../../../../docs/33-effect-polymorphism-and-list-patterns-report.md) made true of
326 // a *user's* definitions too. A pure caller of `list_fold` stays pure however another
327 // caller uses it.
328 (
329 "list_get",
330 Prim::ListGet,
331 poly(
332 &[A],
333 fun(vec![Ty::list(v(A)), int.clone()], Ty::option(v(A))),
334 ),
335 ),
336 (
337 "list_slice",
338 Prim::ListSlice,
339 poly(
340 &[A],
341 fun(
342 vec![Ty::list(v(A)), int.clone(), int.clone()],
343 Ty::list(v(A)),
344 ),
345 ),
346 ),
347 (
348 "list_reverse",
349 Prim::ListReverse,
350 poly(&[A], fun(vec![Ty::list(v(A))], Ty::list(v(A)))),
351 ),
352 (
353 "list_take",
354 Prim::ListTake,
355 poly(&[A], fun(vec![Ty::list(v(A)), int.clone()], Ty::list(v(A)))),
356 ),
357 (
358 "list_drop",
359 Prim::ListDrop,
360 poly(&[A], fun(vec![Ty::list(v(A)), int.clone()], Ty::list(v(A)))),
361 ),
362 (
363 "list_contains",
364 Prim::ListContains,
365 poly(&[A], fun(vec![Ty::list(v(A)), v(A)], bool_.clone())),
366 ),
367 (
368 "list_index_of",
369 Prim::ListIndexOf,
370 poly(
371 &[A],
372 fun(vec![Ty::list(v(A)), v(A)], Ty::option(int.clone())),
373 ),
374 ),
375 (
376 "list_append",
377 Prim::ListAppend,
378 poly(&[A], fun(vec![Ty::list(v(A)), v(A)], Ty::list(v(A)))),
379 ),
380 // Zip *with* a function rather than zip into a pair: Beck has no tuple type, and inventing
381 // one for this would be a language change hiding inside a library addition. The shorter
382 // list decides the length, which is the convention every language that has this agrees on.
383 (
384 "list_zip_with",
385 Prim::ListZip,
386 poly_eff(
387 &[A, B, C],
388 &[E],
389 fun_eff(
390 vec![
391 Ty::list(v(A)),
392 Ty::list(v(B)),
393 fun_eff(vec![v(A), v(B)], v(C), Row::var(E)),
394 ],
395 Ty::list(v(C)),
396 Row::var(E),
397 ),
398 ),
399 ),
400 (
401 "list_fold",
402 Prim::ListFold,
403 poly_eff(
404 &[A, B],
405 &[E],
406 fun_eff(
407 vec![
408 Ty::list(v(A)),
409 v(B),
410 fun_eff(vec![v(B), v(A)], v(B), Row::var(E)),
411 ],
412 v(B),
413 Row::var(E),
414 ),
415 ),
416 ),
417 (
418 "list_all",
419 Prim::ListAll,
420 poly_eff(
421 &[A],
422 &[E],
423 fun_eff(
424 vec![
425 Ty::list(v(A)),
426 fun_eff(vec![v(A)], bool_.clone(), Row::var(E)),
427 ],
428 bool_.clone(),
429 Row::var(E),
430 ),
431 ),
432 ),
433 (
434 "list_any",
435 Prim::ListAny,
436 poly_eff(
437 &[A],
438 &[E],
439 fun_eff(
440 vec![
441 Ty::list(v(A)),
442 fun_eff(vec![v(A)], bool_.clone(), Row::var(E)),
443 ],
444 bool_.clone(),
445 Row::var(E),
446 ),
447 ),
448 ),
449 (
450 "list_flat_map",
451 Prim::ListFlatMap,
452 poly_eff(
453 &[A, B],
454 &[E],
455 fun_eff(
456 vec![
457 Ty::list(v(A)),
458 fun_eff(vec![v(A)], Ty::list(v(B)), Row::var(E)),
459 ],
460 Ty::list(v(B)),
461 Row::var(E),
462 ),
463 ),
464 ),
465 (
466 "map_keys",
467 Prim::MapKeys,
468 poly(&[A, B], fun(vec![Ty::map(v(A), v(B))], Ty::list(v(A)))),
469 ),
470 (
471 "map_merge",
472 Prim::MapMerge,
473 poly(
474 &[A, B],
475 fun(
476 vec![Ty::map(v(A), v(B)), Ty::map(v(A), v(B))],
477 Ty::map(v(A), v(B)),
478 ),
479 ),
480 ),
481 // ------------------------------------------------------------------------ JSON and time
482 //
483 // `json_parse` and `time_parse` **raise** rather than returning a `Result`, and that is the
484 // whole reason [`08`](../../../../../docs/08-roadmap.md) §8.5.3's trap 2 said the standard
485 // library had to wait for [`45`](../../../../../docs/45-error-rows-report.md). A caller who
486 // wants a `Result` writes `try:`; a caller already inside something fallible writes
487 // nothing. Had these been written first, every one of their signatures would have had to
488 // change.
489 (
490 "json_parse",
491 Prim::JsonParse,
492 Scheme::mono(fun_eff(
493 vec![str_.clone()],
494 Ty::con("Json"),
495 Row::of([Effect::Raises(Arc::from("JsonError"))]),
496 )),
497 ),
498 (
499 "json_render",
500 Prim::JsonRender,
501 Scheme::mono(fun(vec![Ty::con("Json")], str_.clone())),
502 ),
503 // RFC 3339 in UTC, and only that: a time zone is a database with a release schedule, and
504 // one is not being embedded in a compiler on the way past. `now()` gives the milliseconds
505 // these two are the calendar over.
506 (
507 "time_format",
508 Prim::TimeFormat,
509 Scheme::mono(fun(vec![int.clone()], str_.clone())),
510 ),
511 (
512 "time_parse",
513 Prim::TimeParse,
514 Scheme::mono(fun_eff(
515 vec![str_.clone()],
516 int.clone(),
517 Row::of([Effect::Raises(Arc::from("TimeError"))]),
518 )),
519 ),
520 // ------------------------------------------------- digests, encodings and identifiers
521 //
522 // Wave 2's crypto item, host half. A hash function is a table and base64 is a grammar, so
523 // both are here rather than in `lib/`; what a program *does* with a digest — a token, a
524 // fingerprint, a check that reads two halves apart — is `lib/crypto.beck`.
525 //
526 // A digest is **pure**. That is the line between this group and `uuid()`/`now()`, which are
527 // the two nondeterministic things a crypto library is usually asked for: the same input
528 // digests to the same string on every replay, so nothing here has to be recorded on an
529 // envelope, and §3.7's rule about folds does not reach it.
530 (
531 "digest",
532 Prim::Digest,
533 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
534 ),
535 // The one function whose input is a `secret[Str]` and whose output is a `Str`.
536 //
537 // A message authentication code exists to be given to somebody who must not learn the key,
538 // so the declassification is what the operation *is* rather than a hole in §3.5. It is
539 // charged `cap.sign` for the reason `reveal` is charged `cap.internal`: no client tier
540 // discharges a capability, so a view cannot mint a token, and a server that mints one has
541 // said so in its row. `adr/0014` is the decision and `security.rs` is the gate that keeps
542 // this the *only* one.
543 (
544 "digest_keyed",
545 Prim::DigestKeyed,
546 Scheme::mono(fun_eff(
547 vec![Ty::secret(str_.clone()), str_.clone()],
548 str_.clone(),
549 Row::of([Effect::Cap(Arc::from("sign"))]),
550 )),
551 ),
552 // Comparing a digest with `==` returns at the first differing byte, which tells whoever is
553 // guessing how much of their guess was right. This does not.
554 (
555 "digest_eq",
556 Prim::DigestEq,
557 Scheme::mono(fun(vec![str_.clone(), str_.clone()], bool_.clone())),
558 ),
559 (
560 "hex_encode",
561 Prim::HexEncode,
562 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
563 ),
564 (
565 "hex_decode",
566 Prim::HexDecode,
567 Scheme::mono(fun_eff(
568 vec![str_.clone()],
569 str_.clone(),
570 Row::of([Effect::Raises(Arc::from("EncodingError"))]),
571 )),
572 ),
573 // RFC 4648 §5 — the URL-safe alphabet, unpadded — because every place a Beck program puts
574 // one of these is a place `+`, `/` and `=` have to be escaped.
575 (
576 "base64_encode",
577 Prim::Base64Encode,
578 Scheme::mono(fun(vec![str_.clone()], str_.clone())),
579 ),
580 (
581 "base64_decode",
582 Prim::Base64Decode,
583 Scheme::mono(fun_eff(
584 vec![str_.clone()],
585 str_.clone(),
586 Row::of([Effect::Raises(Arc::from("EncodingError"))]),
587 )),
588 ),
589 // `uuid()` has minted one since Phase 1 and nothing has ever read one back. This
590 // *normalises* rather than only validating: two spellings of one identifier must not be
591 // two map keys, and a `Str` that has been through here is canonical.
592 (
593 "uuid_parse",
594 Prim::UuidParse,
595 Scheme::mono(fun_eff(
596 vec![str_.clone()],
597 str_.clone(),
598 Row::of([Effect::Raises(Arc::from("UuidError"))]),
599 )),
600 ),
601 (
602 "uuid_version",
603 Prim::UuidVersion,
604 Scheme::mono(fun_eff(
605 vec![str_.clone()],
606 int.clone(),
607 Row::of([Effect::Raises(Arc::from("UuidError"))]),
608 )),
609 ),
610 // ------------------------------------------------------------------------ the outbound call
611 //
612 // The row here is half of the truth, and the half that is a constant. `net.out(host)` is
613 // charged at the *call site* from the literal first argument (`check::prim_call`), because
614 // an effect atom whose argument is a value is the one thing this language has no way to
615 // write in a scheme — and because the egress policy §6.5 derives is exactly the set of
616 // those atoms, so a host that were not written where the call is would not be derivable.
617 (
618 "http_fetch",
619 Prim::HttpFetch,
620 Scheme::mono(fun_eff(
621 vec![str_.clone(), Ty::con("HttpRequest")],
622 Ty::con("HttpResponse"),
623 Row::of([Effect::Raises(Arc::from("HttpError"))]),
624 )),
625 ),
626 (
627 "str_is_empty",
628 Prim::StrIsEmpty,
629 Scheme::mono(fun(vec![str_.clone()], bool_.clone())),
630 ),
631 (
632 "list_len",
633 Prim::ListLen,
634 poly(&[A], fun(vec![Ty::list(v(A))], int.clone())),
635 ),
636 (
637 "list_is_empty",
638 Prim::ListIsEmpty,
639 poly(&[A], fun(vec![Ty::list(v(A))], bool_.clone())),
640 ),
641 // §3.2, verbatim: `map : (list[a], (a -> b ! e)) -> list[b] ! e`. Mapping a function that
642 // touches the dom over a list touches the dom; mapping a pure one does not.
643 (
644 "map_list",
645 Prim::MapList,
646 poly_eff(
647 &[A, B],
648 &[E],
649 fun_eff(
650 vec![Ty::list(v(A)), fun_eff(vec![v(A)], v(B), Row::var(E))],
651 Ty::list(v(B)),
652 Row::var(E),
653 ),
654 ),
655 ),
656 (
657 "filter_list",
658 Prim::FilterList,
659 poly_eff(
660 &[A],
661 &[E],
662 fun_eff(
663 vec![
664 Ty::list(v(A)),
665 fun_eff(vec![v(A)], bool_.clone(), Row::var(E)),
666 ],
667 Ty::list(v(A)),
668 Row::var(E),
669 ),
670 ),
671 ),
672 (
673 "concat_lists",
674 Prim::ConcatLists,
675 poly(&[A], fun(vec![Ty::list(Ty::list(v(A)))], Ty::list(v(A)))),
676 ),
677 (
678 "sort_by",
679 Prim::SortBy,
680 poly_eff(
681 &[A, B],
682 &[E],
683 fun_eff(
684 vec![Ty::list(v(A)), fun_eff(vec![v(A)], v(B), Row::var(E))],
685 Ty::list(v(A)),
686 Row::var(E),
687 ),
688 ),
689 ),
690 (
691 "map_get",
692 Prim::MapGet,
693 poly(
694 &[A, B],
695 fun(vec![Ty::map(v(A), v(B)), v(A)], Ty::option(v(B))),
696 ),
697 ),
698 (
699 "map_insert",
700 Prim::MapInsert,
701 poly(
702 &[A, B],
703 fun(vec![Ty::map(v(A), v(B)), v(A), v(B)], Ty::map(v(A), v(B))),
704 ),
705 ),
706 (
707 "map_remove",
708 Prim::MapRemove,
709 poly(
710 &[A, B],
711 fun(vec![Ty::map(v(A), v(B)), v(A)], Ty::map(v(A), v(B))),
712 ),
713 ),
714 (
715 "map_values",
716 Prim::MapValues,
717 poly(&[A, B], fun(vec![Ty::map(v(A), v(B))], Ty::list(v(B)))),
718 ),
719 (
720 "map_contains",
721 Prim::MapContains,
722 poly(&[A, B], fun(vec![Ty::map(v(A), v(B)), v(A)], bool_.clone())),
723 ),
724 (
725 "map_len",
726 Prim::MapLen,
727 poly(&[A, B], fun(vec![Ty::map(v(A), v(B))], int.clone())),
728 ),
729 (
730 "is_some",
731 Prim::OptionIsSome,
732 poly(&[A], fun(vec![Ty::option(v(A))], bool_.clone())),
733 ),
734 (
735 "unwrap_or",
736 Prim::OptionUnwrapOr,
737 poly(&[A], fun(vec![Ty::option(v(A)), v(A)], v(A))),
738 ),
739 (
740 "html_el",
741 Prim::HtmlEl,
742 Scheme::mono(fun(
743 vec![str_.clone(), Ty::list(attr.clone()), Ty::list(html.clone())],
744 html.clone(),
745 )),
746 ),
747 (
748 "html_text",
749 Prim::HtmlText,
750 poly(&[A], fun(vec![v(A)], html.clone())),
751 ),
752 (
753 "html_attr",
754 Prim::HtmlAttr,
755 poly(&[A], fun(vec![str_.clone(), v(A)], attr.clone())),
756 ),
757 (
758 "html_on",
759 Prim::HtmlOn,
760 poly(&[A], fun(vec![str_.clone(), v(A)], attr.clone())),
761 ),
762 (
763 "html_key",
764 Prim::HtmlKey,
765 poly(&[A], fun(vec![v(A)], attr.clone())),
766 ),
767 (
768 "uuid",
769 Prim::NewUuid,
770 Scheme::mono(fun_eff(vec![], str_.clone(), Row::of([Effect::Nondet]))),
771 ),
772 // The other half of §3.7's forbidden pair. `now()` is legal anywhere a clock exists and
773 // illegal inside a fold — which is a statement about its row, not about its name.
774 (
775 "now",
776 Prim::Now,
777 Scheme::mono(fun_eff(vec![], int.clone(), Row::of([Effect::Nondet]))),
778 ),
779 // §3.5's `type ApiKey = secret[str]`, given a source. Reading the process environment is
780 // `env`, which no client discharges — so a secret cannot even be *obtained* on the tier it
781 // must not reach, before Sendable is consulted at the boundary.
782 (
783 "secret_env",
784 Prim::SecretEnv,
785 Scheme::mono(fun_eff(
786 vec![str_.clone()],
787 Ty::secret(str_.clone()),
788 Row::of([Effect::Env]),
789 )),
790 ),
791 // §3.5's missing quadrant: storable, never Sendable.
792 //
793 // Wrapping is pure and free — recording a fact is not an effect. *Reading* one performs
794 // `cap.internal`, which no tier but the server discharges and which
795 // [`crate::secure`] discharges only inside the authority chokepoint. So a view cannot
796 // unwrap one to render it: not because rendering is forbidden, but because the view is not
797 // somewhere a capability is held.
798 (
799 "internal_of",
800 Prim::InternalOf,
801 poly(&[A], fun(vec![v(A)], Ty::internal(v(A)))),
802 ),
803 (
804 "reveal",
805 Prim::Reveal,
806 poly(
807 &[A],
808 fun_eff(
809 vec![Ty::internal(v(A))],
810 v(A),
811 Row::of([Effect::Cap(Arc::from("internal"))]),
812 ),
813 ),
814 ),
815 // ---- the signal vocabulary (§3.7) ----
816 //
817 // `merge_clients : () -> Stream[(Session × Command)] ! { ingress }`. Phase 1 has no tuple
818 // type, so the pair is the `Proposal` model the prelude declares below — the same shape,
819 // named.
820 (
821 "merge_clients",
822 Prim::MergeClients,
823 Scheme::mono(fun_eff(
824 vec![],
825 Ty::stream(Ty::con("Proposal")),
826 Row::of([Effect::Ingress]),
827 )),
828 ),
829 // `presence : () -> Signal[Map[Str, Int]] ! { cap.presence }` — D6's "who is connected
830 // now, as a first-class non-durable `Signal`".
831 //
832 // A map from actor to how many connections that actor has open, rather than a declared
833 // model: `corpus/15-presence.beck` had already written `here: Map[Str, Int]` by hand, the
834 // ordering is the key's and therefore a function of the value (`docs/54`), and every
835 // question a page asks of it — how many, who, is this one here — is a `Map` primitive that
836 // already exists.
837 (
838 "presence",
839 Prim::Presence,
840 Scheme::mono(fun_eff(
841 vec![],
842 Ty::signal(Ty::map(Ty::str_(), Ty::int())),
843 Row::of([Effect::Cap(Arc::from("presence"))]),
844 )),
845 ),
846 (
847 "filter_map",
848 Prim::StreamFilterMap,
849 poly_eff(
850 &[A, B],
851 &[E],
852 fun_eff(
853 vec![
854 Ty::stream(v(A)),
855 fun_eff(vec![v(A)], Ty::option(v(B)), Row::var(E)),
856 ],
857 Ty::stream(v(B)),
858 Row::var(E),
859 ),
860 ),
861 ),
862 // §3.7: "`fold`'s function must be *replay-pure*: effect row ⊆ {}". That could be written
863 // as a closed empty row here, and unification would reject an impure fold — with a message
864 // about rows failing to unify. The row is a *variable* instead, so the row is inferred and
865 // then judged by `place`, which can say which effect, where it came from, and why the rule
866 // exists. A checked property is worth no more than the diagnostic that delivers it.
867 (
868 "fold",
869 Prim::Fold,
870 poly_eff(
871 &[A, B],
872 &[E],
873 fun(
874 vec![
875 fun_eff(
876 vec![v(A), Ty::app(Ty::ENVELOPE, vec![v(B)])],
877 v(A),
878 Row::var(E),
879 ),
880 v(A),
881 Ty::stream(v(B)),
882 ],
883 Ty::signal(v(A)),
884 ),
885 ),
886 ),
887 (
888 "durable",
889 Prim::Durable,
890 poly(
891 &[A],
892 fun_eff(
893 vec![Ty::signal(v(A))],
894 Ty::signal(v(A)),
895 Row::of([Effect::Durable]),
896 ),
897 ),
898 ),
899 // A signal edge carries its function's row to the signal, which is what makes a view that
900 // reaches the log a *placement* error on the client rather than a runtime surprise.
901 (
902 "signal_map",
903 Prim::SignalMap,
904 poly_eff(
905 &[A, B],
906 &[E],
907 fun_eff(
908 vec![Ty::signal(v(A)), fun_eff(vec![v(A)], v(B), Row::var(E))],
909 Ty::signal(v(B)),
910 Row::var(E),
911 ),
912 ),
913 ),
914 (
915 "map2",
916 Prim::SignalMap2,
917 poly_eff(
918 &[A, B, C],
919 &[E],
920 fun_eff(
921 vec![
922 fun_eff(vec![v(A), v(B)], v(C), Row::var(E)),
923 Ty::signal(v(A)),
924 Ty::signal(v(B)),
925 ],
926 Ty::signal(v(C)),
927 Row::var(E),
928 ),
929 ),
930 ),
931 (
932 "per_session",
933 Prim::PerSession,
934 poly_eff(
935 &[A, B],
936 &[E],
937 fun_eff(
938 vec![
939 Ty::signal(v(A)),
940 fun_eff(vec![v(A), Ty::con("Session")], v(B), Row::var(E)),
941 ],
942 Ty::signal(v(B)),
943 Row::var(E),
944 ),
945 ),
946 ),
947 // `validate : (Session, Command) -> list[Event]` (§3.7), with the accumulator threaded so
948 // that client-minted ids can be checked for freshness and ownership against the actor —
949 // the two obligations F2 puts on validation and the todo sketch deliberately skips.
950 (
951 "decide",
952 Prim::Decide,
953 poly_eff(
954 &[A, B, C],
955 &[E],
956 fun_eff(
957 vec![
958 Ty::stream(Ty::con("Proposal")),
959 Ty::signal(v(A)),
960 fun_eff(
961 vec![v(A), Ty::con("Proposal")],
962 Ty::app(Ty::RESULT, vec![Ty::list(v(B)), v(C)]),
963 Row::var(E),
964 ),
965 ],
966 Ty::stream(v(B)),
967 Row::var(E),
968 ),
969 ),
970 ),
971 ]
972}
973
974/// Types every program has: `Option`, `Result`, `Envelope`, `Session`, `Proposal`.
975///
976/// `Envelope` is §3.7's, field for field — "`seq`: position in the total order — assigned here,
977/// nowhere else; `at`: wall-clock, captured as data; `actor`: stable authenticated identity —
978/// **never** the live `Session` capability or token".
979pub fn types() -> BTreeMap<Arc<str>, TyDecl> {
980 let mut out = BTreeMap::new();
981 let mut add = |d: TyDecl| {
982 out.insert(d.name().clone(), d);
983 };
984
985 add(TyDecl::Union {
986 name: Arc::from(Ty::OPTION),
987 params: vec![Arc::from("T")],
988 variants: vec![
989 Variant {
990 name: Arc::from("Some"),
991 fields: vec![(Arc::from("value"), Ty::Var(A))],
992 },
993 Variant {
994 name: Arc::from("None"),
995 fields: vec![],
996 },
997 ],
998 });
999 add(TyDecl::Union {
1000 name: Arc::from(Ty::RESULT),
1001 params: vec![Arc::from("T"), Arc::from("E")],
1002 variants: vec![
1003 Variant {
1004 name: Arc::from("Ok"),
1005 fields: vec![(Arc::from("value"), Ty::Var(A))],
1006 },
1007 Variant {
1008 name: Arc::from("Err"),
1009 fields: vec![(Arc::from("error"), Ty::Var(B))],
1010 },
1011 ],
1012 });
1013 // JSON as data, so a program reads a document with `match` and builds one with ordinary
1014 // constructors. There is no reflection and no derive: `Json` is a union like any other, and
1015 // turning a `model` into one is a function somebody writes — which is what `@derive` is for
1016 // when it exists, and is not a reason to put a second kind of value in the language now.
1017 //
1018 // The variants are prefixed because a union's constructors are global names: `Str` and `Bool`
1019 // are taken, and `List` would be taken by anybody's own union the day they wrote one.
1020 add(TyDecl::Union {
1021 name: Arc::from("Json"),
1022 params: Vec::new(),
1023 variants: vec![
1024 Variant {
1025 name: Arc::from("JsonNull"),
1026 fields: vec![],
1027 },
1028 Variant {
1029 name: Arc::from("JsonBool"),
1030 fields: vec![(Arc::from("value"), Ty::bool_())],
1031 },
1032 // One number type, and it is the `Float` §32 built rather than a second numeric
1033 // tower: JSON's own grammar has one, and a reader who wants an integer asks for one.
1034 Variant {
1035 name: Arc::from("JsonNumber"),
1036 fields: vec![(Arc::from("value"), Ty::con(Ty::FLOAT))],
1037 },
1038 Variant {
1039 name: Arc::from("JsonStr"),
1040 fields: vec![(Arc::from("value"), Ty::str_())],
1041 },
1042 Variant {
1043 name: Arc::from("JsonList"),
1044 fields: vec![(Arc::from("items"), Ty::list(Ty::con("Json")))],
1045 },
1046 Variant {
1047 name: Arc::from("JsonObject"),
1048 fields: vec![(Arc::from("fields"), Ty::map(Ty::str_(), Ty::con("Json")))],
1049 },
1050 ],
1051 });
1052 // The error `json_parse` raises. A declared type rather than a `Str`, because
1053 // `docs/45` §45.1's atom names the type and a `raises(Str)` would make every string failure in
1054 // a program the same failure.
1055 add(TyDecl::Union {
1056 name: Arc::from("JsonError"),
1057 params: Vec::new(),
1058 variants: vec![Variant {
1059 name: Arc::from("BadJson"),
1060 fields: vec![(Arc::from("why"), Ty::str_())],
1061 }],
1062 });
1063 // The outbound call's three types. A request carries a port and *not* a host: the host is the
1064 // atom the call site performs, so it is an argument of `http_fetch` rather than a field
1065 // anything can compute.
1066 add(TyDecl::Model {
1067 name: Arc::from("HttpRequest"),
1068 params: Vec::new(),
1069 fields: vec![
1070 (Arc::from("method"), Ty::str_()),
1071 // Origin-form, sent as written: `/v1/todos?limit=10`. Nothing percent-encodes it,
1072 // because only the program that built it knows which part of it was data.
1073 (Arc::from("path"), Ty::str_()),
1074 // One value per name, which loses a repeated header (`Set-Cookie`). Said out loud
1075 // here rather than discovered: a `map` is what a program wants to read, and the day a
1076 // caller needs the repeats this becomes a `list[(Str, Str)]` and every reader changes.
1077 (Arc::from("headers"), Ty::map(Ty::str_(), Ty::str_())),
1078 (Arc::from("body"), Ty::str_()),
1079 (Arc::from("port"), Ty::int()),
1080 // Whether the exchange is inside a TLS session whose certificate names the host the
1081 // call site wrote. A field of the request rather than a mode of the client: a program
1082 // may reach one peer over a plaintext hop inside its own cluster and another across
1083 // the internet, and the two calls are two requests.
1084 (Arc::from("tls"), Ty::bool_()),
1085 // Headers whose *value* is a secret, kept apart from the ones whose value is a `Str`.
1086 //
1087 // §3.5 makes a `secret[T]` unreadable — there is no `reveal` for one, which is the
1088 // whole claim — so `"Bearer " + key` cannot be written and an authenticated request
1089 // would be inexpressible. These are merged into the headers by the runtime at the
1090 // edge, so the credential goes on the wire without ever having been a `Str` the
1091 // program could put somewhere else. A request carrying one is not Sendable, which is
1092 // the property doing the work: it cannot be built on, or sent to, a client.
1093 (
1094 Arc::from("secrets"),
1095 Ty::map(Ty::str_(), Ty::secret(Ty::str_())),
1096 ),
1097 ],
1098 });
1099 add(TyDecl::Model {
1100 name: Arc::from("HttpResponse"),
1101 params: Vec::new(),
1102 fields: vec![
1103 (Arc::from("status"), Ty::int()),
1104 (Arc::from("headers"), Ty::map(Ty::str_(), Ty::str_())),
1105 (Arc::from("body"), Ty::str_()),
1106 ],
1107 });
1108 // A status is a reply and not a failure — a 500 arrived, and a program that treats it as an
1109 // exception has lost the body that says why. These are the cases where *nothing* arrived,
1110 // plus the one the library raises when a caller asks for a status it did not get.
1111 add(TyDecl::Union {
1112 name: Arc::from("HttpError"),
1113 params: Vec::new(),
1114 variants: vec![
1115 Variant {
1116 name: Arc::from("HttpUnreachable"),
1117 fields: vec![
1118 (Arc::from("host"), Ty::str_()),
1119 (Arc::from("why"), Ty::str_()),
1120 ],
1121 },
1122 Variant {
1123 name: Arc::from("HttpTimedOut"),
1124 fields: vec![
1125 (Arc::from("host"), Ty::str_()),
1126 (Arc::from("millis"), Ty::int()),
1127 ],
1128 },
1129 Variant {
1130 name: Arc::from("HttpBadResponse"),
1131 fields: vec![(Arc::from("why"), Ty::str_())],
1132 },
1133 // Raised by `lib/http.beck`'s `require_ok`, never by the primitive.
1134 Variant {
1135 name: Arc::from("HttpStatus"),
1136 fields: vec![
1137 (Arc::from("status"), Ty::int()),
1138 (Arc::from("body"), Ty::str_()),
1139 ],
1140 },
1141 ],
1142 });
1143 add(TyDecl::Union {
1144 name: Arc::from("TimeError"),
1145 params: Vec::new(),
1146 variants: vec![Variant {
1147 name: Arc::from("BadTime"),
1148 fields: vec![(Arc::from("why"), Ty::str_())],
1149 }],
1150 });
1151 // The two the decoders raise. Separate types rather than one `BadInput`, because a caller
1152 // reading a base64 field and a caller reading an identifier are recovering from different
1153 // things: the first re-reads the message, the second rejects the request.
1154 add(TyDecl::Union {
1155 name: Arc::from("EncodingError"),
1156 params: Vec::new(),
1157 variants: vec![Variant {
1158 name: Arc::from("BadEncoding"),
1159 fields: vec![
1160 (Arc::from("encoding"), Ty::str_()),
1161 (Arc::from("why"), Ty::str_()),
1162 ],
1163 }],
1164 });
1165 add(TyDecl::Union {
1166 name: Arc::from("UuidError"),
1167 params: Vec::new(),
1168 variants: vec![Variant {
1169 name: Arc::from("BadUuid"),
1170 fields: vec![(Arc::from("why"), Ty::str_())],
1171 }],
1172 });
1173 add(TyDecl::Model {
1174 name: Arc::from(Ty::ENVELOPE),
1175 params: vec![Arc::from("T")],
1176 fields: vec![
1177 (Arc::from("seq"), Ty::int()),
1178 (Arc::from("at"), Ty::int()),
1179 (Arc::from("actor"), Ty::str_()),
1180 (Arc::from("body"), Ty::Var(A)),
1181 ],
1182 });
1183 // "`Session` is minted by the identity subsystem … with verified claims mapped to typed
1184 // capabilities" (§3.7).
1185 //
1186 // `claims` is the second half, and it is a `map[Str, Str]` rather than a type per issuer: what
1187 // an issuer emits is that issuer's decision, so a program that reads `session.claims` is
1188 // reading somebody else's vocabulary and the type says so. It is **empty** under a provider
1189 // that verifies nothing, which is what makes `map_get(session.claims, "role")` a check rather
1190 // than a decoration.
1191 //
1192 // It does not reach the log: an `Envelope` carries `actor` and nothing else, because a fold
1193 // whose replay depended on what the issuer was saying at the time would not be a fold
1194 // (`docs/95` §95.4).
1195 add(TyDecl::Model {
1196 name: Arc::from("Session"),
1197 params: Vec::new(),
1198 fields: vec![
1199 (Arc::from("actor"), Ty::str_()),
1200 (Arc::from("claims"), Ty::map(Ty::str_(), Ty::str_())),
1201 ],
1202 });
1203 add(TyDecl::Model {
1204 name: Arc::from("Proposal"),
1205 params: Vec::new(),
1206 fields: vec![
1207 (Arc::from("session"), Ty::con("Session")),
1208 (Arc::from("command"), Ty::con("Command")),
1209 ],
1210 });
1211 out
1212}
1213
1214/// The traits every program has.
1215///
1216/// One, and it is the one SICP §2.5.1 builds by hand: **generic arithmetic**. The book's answer to
1217/// "how do rationals join a tower that already has integers" is a set of generic operations —
1218/// `add`, `sub`, `mul`, `div` — that each type installs an implementation for, and that is exactly
1219/// a trait. `docs/32` §32.3 resolved `+` from its operands and said an ad-hoc resolution was "the
1220/// honest thing to build before traits exist"; they exist, so `+` resolves through this when its
1221/// operands are neither `Int` nor `Float` nor `Str`.
1222///
1223/// The method names are the book's. A tower is only worth having if a third floor can be added
1224/// from outside the language, and `impl Num for Rational` is how §2.1.1's exercise stops being
1225/// about function names and starts being about data abstraction.
1226///
1227/// It is **not published**: `own_traits` is what a `.becki` carries, and this belongs to the
1228/// language rather than to any module. Nor is it implemented for `Int` or `Float` — those go
1229/// through the primitives, because a tower whose bottom floor is a dictionary call would make every
1230/// existing program slower to prove a point.
1231pub fn traits() -> Vec<TraitSig> {
1232 let binary = |name: &str| MethodSig {
1233 name: Arc::from(name),
1234 params: vec![
1235 (Arc::from("self"), Ty::con(SELF)),
1236 (Arc::from("other"), Ty::con(SELF)),
1237 ],
1238 ret: Ty::con(SELF),
1239 effects: Vec::new(),
1240 };
1241 vec![TraitSig {
1242 name: Arc::from(NUM),
1243 methods: vec![binary("add"), binary("sub"), binary("mul"), binary("div")],
1244 }]
1245}
1246
1247/// The trait `+`, `-`, `*` and `/` resolve through.
1248pub const NUM: &str = "Num";
1249
1250/// The abstract receiver a trait's signatures are written in terms of.
1251const SELF: &str = "Self";
1252
1253/// Which method of [`NUM`] an operator is.
1254pub fn num_method(op: Prim) -> Option<&'static str> {
1255 Some(match op {
1256 Prim::Add => "add",
1257 Prim::Sub => "sub",
1258 Prim::Mul => "mul",
1259 Prim::Div => "div",
1260 _ => return None,
1261 })
1262}
1263
1264/// The type-constructor arities the checker knows without a declaration.
1265pub fn builtin_arity(name: &str) -> Option<usize> {
1266 Some(match name {
1267 Ty::INT | Ty::STR | Ty::BOOL | Ty::FLOAT | Ty::UNIT | Ty::HTML | Ty::ATTR => 0,
1268 Ty::LIST
1269 | Ty::OPTION
1270 | Ty::STREAM
1271 | Ty::SIGNAL
1272 | Ty::ENVELOPE
1273 | Ty::SECRET
1274 | Ty::INTERNAL => 1,
1275 Ty::MAP | Ty::RESULT => 2,
1276 _ => return None,
1277 })
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283
1284 #[test]
1285 fn every_prim_has_a_signature_and_the_names_are_unique() {
1286 let all = prims();
1287 let mut names: Vec<&str> = all.iter().map(|(n, _, _)| *n).collect();
1288 names.sort_unstable();
1289 let before = names.len();
1290 names.dedup();
1291 assert_eq!(before, names.len(), "duplicate prelude name");
1292 for (name, prim, _) in &all {
1293 assert_eq!(*name, prim.name(), "prelude name must match Prim::name");
1294 }
1295 }
1296
1297 #[test]
1298 fn folds_type_the_way_section_3_7_says() {
1299 // `fold(f, init, s) : Signal[S]` where `f : (S, Envelope[E]) -> S`.
1300 let all = prims();
1301 let (_, _, scheme) = all
1302 .iter()
1303 .find(|(n, _, _)| *n == "fold")
1304 .expect("fold exists");
1305 match &scheme.ty {
1306 Ty::Fun(params, ret, _) => {
1307 assert_eq!(params.len(), 3);
1308 assert_eq!(ret.con_name(), Some(Ty::SIGNAL));
1309 assert!(matches!(¶ms[0], Ty::Fun(ps, _, _) if ps.len() == 2));
1310 assert_eq!(params[2].con_name(), Some(Ty::STREAM));
1311 }
1312 other => panic!("fold should be a function, got {other}"),
1313 }
1314 }
1315
1316 #[test]
1317 fn the_standard_library_is_effect_polymorphic_where_section_3_2_says_it_is() {
1318 // "Effect polymorphism is what keeps one standard library." If `map_list` were monomorphic
1319 // in its function's row there would have to be a pure `map` and an effectful `map`, and the
1320 // choice would be the caller's problem rather than the compiler's.
1321 let all = prims();
1322 for name in [
1323 "map_list",
1324 "filter_list",
1325 "sort_by",
1326 "signal_map",
1327 "per_session",
1328 "decide",
1329 ] {
1330 let (_, _, scheme) = all
1331 .iter()
1332 .find(|(n, _, _)| *n == name)
1333 .unwrap_or_else(|| panic!("{name} exists"));
1334 assert!(
1335 !scheme.row_vars.is_empty(),
1336 "`{name}` takes a function, so it must be polymorphic in that function's row"
1337 );
1338 }
1339 // …and the effectful primitives carry their atom, closed.
1340 for (name, atom) in [
1341 ("merge_clients", Effect::Ingress),
1342 ("durable", Effect::Durable),
1343 ("uuid", Effect::Nondet),
1344 ("now", Effect::Nondet),
1345 ("secret_env", Effect::Env),
1346 ] {
1347 let (_, _, scheme) = all.iter().find(|(n, _, _)| *n == name).unwrap();
1348 let Ty::Fun(_, _, row) = &scheme.ty else {
1349 panic!("{name} is a function")
1350 };
1351 assert!(
1352 row.atoms.contains(&atom),
1353 "`{name}` should perform `{atom}`"
1354 );
1355 }
1356 }
1357
1358 #[test]
1359 fn every_primitives_own_atoms_agree_with_its_scheme() {
1360 // Two statements of the same fact — the table `Prim::effects` returns and the row in the
1361 // scheme — so a primitive cannot acquire an effect in one and not the other.
1362 for (name, prim, scheme) in prims() {
1363 let Ty::Fun(_, _, row) = &scheme.ty else {
1364 continue;
1365 };
1366 for e in &prim.effects() {
1367 assert!(
1368 row.atoms.contains(e),
1369 "`{name}` performs `{e}` but its scheme does not say so"
1370 );
1371 }
1372 for e in &row.atoms {
1373 assert!(
1374 prim.effects().contains(e),
1375 "`{name}`'s scheme carries `{e}` but `Prim::effects` does not"
1376 );
1377 }
1378 }
1379 }
1380
1381 #[test]
1382 fn the_envelope_carries_an_actor_and_never_a_session() {
1383 let ts = types();
1384 match ts.get(Ty::ENVELOPE).expect("Envelope exists") {
1385 TyDecl::Model { fields, .. } => {
1386 let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_ref()).collect();
1387 assert_eq!(names, ["seq", "at", "actor", "body"]);
1388 assert!(!names.contains(&"session"), "F5: no capability in the log");
1389 }
1390 other => panic!("Envelope should be a model, got {other:?}"),
1391 }
1392 }
1393}