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!(&params[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}