beck_core/core.rs
1//! `Core` — the load-bearing IR, and the evaluator that runs it.
2//!
3//! [`docs/04-compiler-architecture.md`](../../../../../docs/04-compiler-architecture.md) §4.2:
4//! "Typed ANF/SSA hybrid; explicit closures, explicit effect operations, explicit tier annotation
5//! per node; `Query` sub-language kept *symbolic*. Typechecked semantics, placement, splitting,
6//! optimisation. The load-bearing IR."
7//!
8//! Two things stay symbolic here, exactly as §4.2 demands, and for the same stated reason —
9//! lowering them early would foreclose Phase 3:
10//!
11//! * **Signal and stream operations** are [`Prim`]s ([`Prim::Fold`], [`Prim::Durable`],
12//! [`Prim::SignalMap`], …), not loops. A fold that has already become a loop cannot be compiled
13//! to an incremental dataflow plan.
14//! * **UI trees** are `Html` *values* built by [`Prim::HtmlEl`] and friends, not DOM mutation
15//! calls, so the same value can be server-side rendered, diffed, or (Phase 3) compiled for the
16//! client.
17//!
18//! # On the backend
19//!
20//! The roadmap names Cranelift as Phase 1's server backend. What is here instead is a **`Core`
21//! evaluator**: the "engine-in-Rust with the language as its configuration" route that
22//! [`docs/00-original-idea.md`](../../../../../docs/00-original-idea.md) names as one of the three
23//! that work for a GC'd functional language on a Rust host (Materialize's shape). It is the
24//! deliberately-bad-but-complete option, and it keeps the `Core → Target` seam narrow, which §5.2
25//! says is what lets a backend slot in later. The Phase 1 report says plainly that native codegen
26//! is not done.
27
28use std::collections::BTreeSet;
29use std::fmt;
30use std::sync::Arc;
31
32use beck_diag::Span;
33
34use crate::html::Html;
35use crate::pmap::PMap;
36use crate::seq::Seq;
37use crate::ty::{Effect, Tier, Ty};
38
39pub type VarId = u32;
40
41/// A primitive operation. Everything the standard library provides in Phase 1.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum Prim {
44 Add,
45 Sub,
46 Mul,
47 Div,
48 Rem,
49 Neg,
50 /// `abs`, `sqrt` and `Int` → `Float`, which are the three the numeric tower needs before any
51 /// of SICP §1.1.7 will run (`docs/27` §27.2). `Abs` is resolved from its operand the way the
52 /// arithmetic operators are; the other two are monomorphic.
53 Abs,
54 Sqrt,
55 Sin,
56 Cos,
57 Trunc,
58 ToFloat,
59 Eq,
60 Ne,
61 Lt,
62 Le,
63 Gt,
64 Ge,
65 And,
66 Or,
67 Not,
68 ToStr,
69 StrTrim,
70 StrToInt,
71 // ---- strings (docs/27 §? — Wave 2's string half). One primitive each, because a string is
72 // where a language's host has to be asked and there is nothing to express in Beck itself.
73 StrLen,
74 StrSlice,
75 StrSplit,
76 StrJoin,
77 StrContains,
78 StrStartsWith,
79 StrEndsWith,
80 StrUpper,
81 StrLower,
82 StrReplace,
83 StrIndexOf,
84 StrRepeat,
85 StrChars,
86 StrIsEmpty,
87 ListLen,
88 ListIsEmpty,
89 ListMin,
90 ListMax,
91 ListSum,
92 ListUnique,
93 // ---- collections
94 ListGet,
95 ListSlice,
96 ListReverse,
97 ListTake,
98 ListDrop,
99 ListContains,
100 ListIndexOf,
101 ListFold,
102 ListAll,
103 ListAny,
104 ListFlatMap,
105 ListZip,
106 ListAppend,
107 MapKeys,
108 MapMerge,
109 // ---- JSON, and the standard library's first fallible function.
110 //
111 // `json_parse` raises rather than returning a `Result`, which is the shape
112 // [`docs/27`](../../../../../docs/27-the-walls-come-down-report.md) settled and the reason §8.5.3's trap 2
113 // said the library had to wait for it: a caller that wants a `Result` writes `try:`, and a
114 // caller inside something already fallible writes nothing at all.
115 JsonParse,
116 JsonRender,
117 // ---- time. `now()` gives milliseconds; these two are the civil calendar over them.
118 TimeFormat,
119 TimeParse,
120 // ---- digests, encodings and identifiers (`crate::digest`). A hash is a table and base64 is a
121 // grammar, so both are the host's half of `lib/README.md`'s division; a digest is also a *pure*
122 // function, which is what separates this from the other things a crypto library offers.
123 Digest,
124 /// A message authentication code: the one primitive whose input is a `secret[Str]` and whose
125 /// output is a `Str`. Charged `cap.sign` rather than left free, so a view cannot mint one —
126 /// `docs/adr/0014` is the record of the decision and §3.5 is what it is measured against.
127 DigestKeyed,
128 /// Constant-time equality, for the caller comparing a digest against one that arrived.
129 DigestEq,
130 HexEncode,
131 HexDecode,
132 Base64Encode,
133 Base64Decode,
134 /// Validates and *normalises*: two spellings of one identifier must not be two map keys.
135 UuidParse,
136 UuidVersion,
137 // ---- the outbound call. The *second* primitive whose row is a function of its argument
138 // (`Raise` is the first): the host it is given is the `net.out(host)` atom it performs, which
139 // is why that argument has to be written at the call site rather than computed.
140 HttpFetch,
141 MapList,
142 FilterList,
143 ConcatLists,
144 SortBy,
145 MapGet,
146 MapInsert,
147 MapRemove,
148 MapValues,
149 MapContains,
150 MapLen,
151 OptionIsSome,
152 OptionUnwrapOr,
153 HtmlEl,
154 HtmlText,
155 HtmlAttr,
156 HtmlOn,
157 HtmlKey,
158 /// Mints a fresh id. Nondeterministic, so §3.7 forbids it inside a fold; the client mints
159 /// entity ids instead, which is "the small tell that browsers here are replicas, not
160 /// terminals".
161 NewUuid,
162 /// Reads the wall clock. The other half of §3.7's rule: "time is data on the envelope".
163 Now,
164 /// Reads a secret from the process environment, yielding a `secret[Str]` (§3.5).
165 SecretEnv,
166 /// `raise e` — fail with a value.
167 ///
168 /// The atom it performs is `raises(T)`, which depends on the *type* of its argument, so the
169 /// checker attaches it where that type is known rather than [`Prim::effects`] declaring it.
170 /// This is the first primitive whose row is not a constant, and it is why that table's doc
171 /// says "the atoms this primitive performs *itself*".
172 Raise,
173 /// `try: block` — run a thunk, and turn a raise of the named type into an `Err`.
174 ///
175 /// Two arguments: the thunk, and the name of the error type this handler catches. The name is
176 /// what stops a handler from catching a failure it cannot type — a caller's function may raise
177 /// something this `try` never heard of, and that has to keep travelling.
178 Try,
179 /// `parallel: block` — run the scope's children, then its tail with their results bound.
180 ///
181 /// The arguments are the children's thunks followed by the continuation, so a child cannot
182 /// outlive the scope: there is no handle, and the only thing that can read a child's result is
183 /// the one lambda the scope built. That is [`docs/38`](../../../../../docs/38-literature-survey.md)
184 /// §38.4's "spawn/await as effect operations, the scope as their handler" with the handler as
185 /// the *only* form — the operations are not separately reachable.
186 ///
187 /// The children are independent by construction (none of them can name another) and no child
188 /// may perform an effect another child could observe, so the scope's answer does not depend on
189 /// the order they ran in. A backend may therefore run them together; running them in the order
190 /// they are written is a correct implementation of that, and is what the tree-walker does.
191 Parallel,
192 /// Wraps a value as `internal[T]`: storable, never Sendable.
193 InternalOf,
194 /// Unwraps one. Performs `cap.internal`, so only the authority chokepoint can do it.
195 Reveal,
196 // ---- the symbolic signal vocabulary (§3.7) ----
197 MergeClients,
198 /// `presence()` — who is connected now, as a signal that is not a function of the log.
199 ///
200 /// D6's last row. It performs `cap.presence` rather than an atom of its own, which is F16
201 /// ([`docs/14`](../../../../../docs/14-review-findings.md)) taken literally: "presence signals
202 /// leak who-is-online; gate behind a capability like any other view". The capability is also
203 /// what places it — no tier below the server discharges a `cap.*` — so a fold cannot read the
204 /// roster and a view reaches it across a declared edge.
205 Presence,
206 /// `awareness(f)` — what every subscriber contributes, keyed by actor.
207 ///
208 /// Presence with a payload, and the same three rules for the same reasons: a non-log input to
209 /// a view, bounded because the key is a name the client chooses (`docs/82` §82.5), and refused
210 /// at the chokepoint because an event whose existence depended on where somebody's cursor was
211 /// would not survive a replay. It carries `cap.presence` rather than a capability of its own —
212 /// a roster with payloads discloses strictly more than a roster, and gating the smaller
213 /// disclosure while leaving the larger one open would be the wrong way round.
214 ///
215 /// `f` is a function of the **`Session`**, which is what the server already holds for every
216 /// connection. `docs/104` §104.8 has the half that needs more than that and why it waits.
217 Awareness,
218 /// `freshness()` — whether the page being rendered is the confirmed state or a guess.
219 ///
220 /// §3.7's "`Signal[T]` carries a freshness dimension (`confirmed | pending(n)`) that UI code
221 /// can render (\"saving…\") — staleness is typed, not pretended away". It is the mirror image
222 /// of [`Prim::Presence`]: presence is a fact the *server* holds about its sockets and cannot
223 /// reach a Mode B client, and freshness is a fact the *client* holds about its own guesses and
224 /// is `Confirmed` everywhere else. No capability, because nothing is disclosed by it — a client
225 /// counting its own unacknowledged commands is reading itself.
226 Freshness,
227 /// `gestures(step, init)` — the non-durable fold
228 /// [`docs/10`](../../../../../docs/10-decisions.md) D30 decides the shape of.
229 ///
230 /// The mirror of [`Prim::MergeClients`] on the other side of the wire: that one is every
231 /// client's proposals arriving at the one place time enters, and this is one client's gestures
232 /// arriving nowhere else at all. **Nothing here is proposed, validated or recorded** — a
233 /// gesture is a movement of the interface, which is why it is neither a `Command` nor an
234 /// `Event`, the two words that already mean something about the log.
235 ///
236 /// It carries `dom`, and that is the whole of its placement: no tier but the client discharges
237 /// it ([`crate::ty::Tier::discharges`]), so this is client-placed by machinery that was already
238 /// there, and `durable` is on a tier the client cannot reach. D3's invariant is untouched
239 /// rather than weakened — replay reproduces everything that was ever in the log, and no gesture
240 /// ever was.
241 Gestures,
242 StreamFilterMap,
243 Fold,
244 Durable,
245 SignalMap,
246 SignalMap2,
247 /// §3.8's per-session view: `todos.map(filter_by(session.user))`. First-class because "the
248 /// fanout cost becomes a first-class engineering concern".
249 PerSession,
250 /// The authority chokepoint: the sole consumer of ingress, holding the accumulator so that
251 /// first-writer-wins and ownership can be decided (§3.7, F2).
252 Decide,
253}
254
255impl Prim {
256 pub fn name(self) -> &'static str {
257 use Prim::*;
258 match self {
259 Add => "+",
260 Sub => "-",
261 Mul => "*",
262 Div => "/",
263 Rem => "%",
264 Neg => "negate",
265 Raise => "raise",
266 Try => "try",
267 Parallel => "parallel",
268 Abs => "abs",
269 Sqrt => "sqrt",
270 Sin => "sin",
271 Cos => "cos",
272 Trunc => "trunc",
273 ToFloat => "float",
274 Eq => "==",
275 Ne => "!=",
276 Lt => "<",
277 Le => "<=",
278 Gt => ">",
279 Ge => ">=",
280 And => "and",
281 Or => "or",
282 Not => "not",
283 ToStr => "str",
284 StrTrim => "str_trim",
285 StrToInt => "str_to_int",
286 StrLen => "str_len",
287 StrSlice => "str_slice",
288 StrSplit => "str_split",
289 StrJoin => "str_join",
290 StrContains => "str_contains",
291 StrStartsWith => "str_starts_with",
292 StrEndsWith => "str_ends_with",
293 StrUpper => "str_upper",
294 StrLower => "str_lower",
295 StrReplace => "str_replace",
296 StrIndexOf => "str_index_of",
297 StrRepeat => "str_repeat",
298 StrChars => "str_chars",
299 ListGet => "list_get",
300 ListSlice => "list_slice",
301 ListReverse => "list_reverse",
302 ListTake => "list_take",
303 ListDrop => "list_drop",
304 ListContains => "list_contains",
305 ListIndexOf => "list_index_of",
306 ListFold => "list_fold",
307 ListAll => "list_all",
308 ListAny => "list_any",
309 ListFlatMap => "list_flat_map",
310 ListZip => "list_zip_with",
311 ListAppend => "list_append",
312 MapKeys => "map_keys",
313 MapMerge => "map_merge",
314 JsonParse => "json_parse",
315 JsonRender => "json_render",
316 TimeFormat => "time_format",
317 TimeParse => "time_parse",
318 Digest => "digest",
319 DigestKeyed => "digest_keyed",
320 DigestEq => "digest_eq",
321 HexEncode => "hex_encode",
322 HexDecode => "hex_decode",
323 Base64Encode => "base64_encode",
324 Base64Decode => "base64_decode",
325 UuidParse => "uuid_parse",
326 UuidVersion => "uuid_version",
327 HttpFetch => "http_fetch",
328 StrIsEmpty => "str_is_empty",
329 ListLen => "list_len",
330 ListIsEmpty => "list_is_empty",
331 ListMin => "list_min",
332 ListMax => "list_max",
333 ListSum => "list_sum",
334 ListUnique => "list_unique",
335 MapList => "map_list",
336 FilterList => "filter_list",
337 ConcatLists => "concat_lists",
338 SortBy => "sort_by",
339 MapGet => "map_get",
340 MapInsert => "map_insert",
341 MapRemove => "map_remove",
342 MapValues => "map_values",
343 MapContains => "map_contains",
344 MapLen => "map_len",
345 OptionIsSome => "is_some",
346 OptionUnwrapOr => "unwrap_or",
347 HtmlEl => "html_el",
348 HtmlText => "html_text",
349 HtmlAttr => "html_attr",
350 HtmlOn => "html_on",
351 HtmlKey => "html_key",
352 NewUuid => "uuid",
353 Now => "now",
354 SecretEnv => "secret_env",
355 InternalOf => "internal_of",
356 Reveal => "reveal",
357 MergeClients => "merge_clients",
358 Presence => "presence",
359 Awareness => "awareness",
360 Freshness => "freshness",
361 Gestures => "gestures",
362 StreamFilterMap => "filter_map",
363 Fold => "fold",
364 Durable => "durable",
365 SignalMap => "signal_map",
366 SignalMap2 => "map2",
367 PerSession => "per_session",
368 Decide => "decide",
369 }
370 }
371
372 /// The atoms this primitive performs *itself*.
373 ///
374 /// The polymorphic half of a primitive's row — `map_list`'s `e`, which is whatever its function
375 /// argument does — lives in the scheme in [`crate::prelude`], because a row variable is not a
376 /// constant. A test holds the two in agreement.
377 pub fn effects(self) -> Vec<Effect> {
378 match self {
379 // "Every connected client's send!s, interleaved. Arbitrary order — this is the
380 // nondeterminism; there is exactly one of these."
381 Prim::MergeClients => vec![Effect::Ingress],
382 // Who is connected is not a function of the log, so this is the second source of
383 // nondeterminism in a Beck program — and the only one a *view* may read. The atom is a
384 // capability rather than a new label: F16 asks for exactly that, and `cap.*` is what
385 // keeps it off the tiers that would make it unreplayable (§3.3).
386 Prim::Presence | Prim::Awareness => {
387 vec![Effect::Cap(std::sync::Arc::from("presence"))]
388 }
389 Prim::Durable => vec![Effect::Durable],
390 // D30: ephemerality is a property of the stream. `dom` is what says so — the client is
391 // the only tier that discharges it, so the state folded from this stream is on the
392 // client, and `durable` is on a tier the client cannot reach.
393 Prim::Gestures => vec![Effect::Dom],
394 Prim::NewUuid | Prim::Now => vec![Effect::Nondet],
395 // The scope performs `spawn` itself; what its children perform is charged by the
396 // checker, from each child's own row, because a thunk's effects belong to the thunk's
397 // type and this is the form that calls them.
398 Prim::Parallel => vec![Effect::Spawn],
399 Prim::SecretEnv => vec![Effect::Env],
400 // Wrapping is free; *reading* is the privileged half, and the capability is what stops
401 // a view unwrapping one to render it.
402 Prim::Reveal => vec![Effect::Cap(std::sync::Arc::from("internal"))],
403 // The standard library's fallible pair. Unlike `Prim::Raise`, whose atom depends on
404 // its argument's type, these two raise a type the prelude declares — so the row *is* a
405 // constant and belongs here, where a test holds it against the scheme.
406 Prim::JsonParse => vec![Effect::Raises(std::sync::Arc::from("JsonError"))],
407 Prim::TimeParse => vec![Effect::Raises(std::sync::Arc::from("TimeError"))],
408 Prim::HexDecode | Prim::Base64Decode => {
409 vec![Effect::Raises(std::sync::Arc::from("EncodingError"))]
410 }
411 Prim::UuidParse | Prim::UuidVersion => {
412 vec![Effect::Raises(std::sync::Arc::from("UuidError"))]
413 }
414 // The declassifier. `digest` and the encodings are pure; this one reads a
415 // `secret[Str]`, so it is held where `reveal` is held — behind a capability no client
416 // tier discharges, which is what stops a view minting a token (`docs/adr/0014`).
417 Prim::DigestKeyed => vec![Effect::Cap(std::sync::Arc::from("sign"))],
418 // Half of `http_fetch`'s row is a constant and half is its first argument. The
419 // constant half is here; the `net.out(host)` half is added by [`Core::effects`],
420 // which can see the argument, and by the checker, which is where it is charged.
421 Prim::HttpFetch => vec![Effect::Raises(std::sync::Arc::from("HttpError"))],
422 _ => Vec::new(),
423 }
424 }
425}
426
427#[derive(Clone, Debug, PartialEq)]
428pub enum Const {
429 Unit,
430 Bool(bool),
431 Int(i64),
432 Float(f64),
433 Str(Arc<str>),
434}
435
436/// One arm of a `match`. Patterns are shallow — a constructor and its named field binders — which
437/// is what §3.1's exhaustiveness check needs and no more.
438#[derive(Clone, Debug)]
439pub struct Arm {
440 pub pattern: Pattern,
441 /// `case Circle(r) if r > 0:` — a condition on the arm, in the scope of what the pattern bound.
442 ///
443 /// A guard that fails falls through to the next arm, which is what makes it a guard rather
444 /// than an `if` in the body.
445 pub guard: Option<Core>,
446 pub body: Core,
447 pub span: Span,
448}
449
450impl Arm {
451 /// Every expression this arm holds, in the order they run.
452 ///
453 /// One method rather than `&a.body` at each of fourteen call sites, for
454 /// [`Pattern::binders`]'s reason and with its history: a guard added as a field those sites
455 /// did not know about would be a `Core` that liveness never marks, that `frames` never counts
456 /// a slot for, and that the plan's free-variable analysis never sees — none of which is a
457 /// compile error, and all of which are wrong on a program that uses one.
458 pub fn exprs(&self) -> impl Iterator<Item = &Core> {
459 self.guard.iter().chain(std::iter::once(&self.body))
460 }
461
462 pub fn exprs_mut(&mut self) -> impl Iterator<Item = &mut Core> {
463 self.guard.iter_mut().chain(std::iter::once(&mut self.body))
464 }
465}
466
467#[derive(Clone, Debug)]
468pub enum Pattern {
469 Wildcard,
470 Bind(VarId),
471 Const(Const),
472 /// `Added(id, text)` — `variant` names the constructor, and each field carries the pattern
473 /// matched against it.
474 ///
475 /// A field's pattern is usually a [`Pattern::Bind`] or a [`Pattern::Wildcard`], which is what
476 /// `Added(id, text)` and `Added(_)` mean. It may be any pattern: `Some(Added(id, text))` is a
477 /// `Ctor` whose one field is a `Ctor`.
478 Ctor {
479 variant: Arc<str>,
480 binds: Vec<(Arc<str>, Pattern)>,
481 },
482 /// `whole @ Circle(r)` — a name for the value, and a pattern that takes it apart.
483 ///
484 /// The binder is irrefutable, so whether this matches is entirely `inner`'s question.
485 At {
486 var: VarId,
487 inner: Box<Pattern>,
488 },
489 /// `Circle(r) | Square(r)` — one of several, and every alternative binds the same names.
490 ///
491 /// The checker unifies the alternatives' binders onto one set of variables, so the body reads
492 /// `r` without knowing which alternative matched. That is the rule that makes an or-pattern a
493 /// pattern rather than two arms sharing a body.
494 Or(Vec<Pattern>),
495 /// `[]`, `[x]`, `[a, b]`, `[first, *rest]` — a list, taken apart.
496 ///
497 /// `items` is one pattern per fixed element and `rest` is the optional tail binder. A pattern
498 /// with no `rest` matches a list of exactly `items.len()` elements; one with a `rest` matches
499 /// any list at least that long.
500 ///
501 /// The tail is a binder rather than a pattern, and deliberately: `[a, *[b, c]]` is `[a, b, c]`
502 /// written twice over, so what it would add is a second spelling rather than a shape.
503 List {
504 items: Vec<Pattern>,
505 rest: Option<Option<VarId>>,
506 },
507}
508
509impl Pattern {
510 /// Every variable this pattern binds, at any depth.
511 ///
512 /// One method rather than a `match` at each of the three call sites, because those three were
513 /// `Bind`/`Ctor`/`_ => {}` — and a new pattern kind falling into the `_` would have been a
514 /// silent miscount in the splitter's variable supply and a false *free* variable in the plan's
515 /// analysis. Neither would have failed a test until a program used one (`docs/27` §27.3).
516 pub fn binders(&self) -> Vec<VarId> {
517 let mut out = Vec::new();
518 self.collect_binders(&mut out);
519 out
520 }
521
522 fn collect_binders(&self, out: &mut Vec<VarId>) {
523 match self {
524 Pattern::Wildcard | Pattern::Const(_) => {}
525 Pattern::Bind(v) => out.push(*v),
526 Pattern::At { var, inner } => {
527 out.push(*var);
528 inner.collect_binders(out);
529 }
530 Pattern::Ctor { binds, .. } => {
531 for (_, p) in binds {
532 p.collect_binders(out);
533 }
534 }
535 Pattern::List { items, rest } => {
536 for p in items {
537 p.collect_binders(out);
538 }
539 out.extend(rest.iter().filter_map(|b| *b));
540 }
541 // Every alternative binds the same variables, so one would do; all of them, deduped,
542 // is what stays right if the checker's unification is ever wrong about that.
543 Pattern::Or(alts) => {
544 for p in alts {
545 p.collect_binders(out);
546 }
547 out.sort_unstable();
548 out.dedup();
549 }
550 }
551 }
552
553 /// Whether this pattern matches every value of its type, so that no arm after it can run.
554 ///
555 /// Only a binder and a wildcard do — and a list pattern of nothing but a tail, `[*rest]`,
556 /// which is the one refutable-looking shape that refuses nothing.
557 pub fn irrefutable(&self) -> bool {
558 match self {
559 Pattern::Wildcard | Pattern::Bind(_) => true,
560 Pattern::List { items, rest } => items.is_empty() && rest.is_some(),
561 Pattern::Or(alts) => alts.iter().any(Pattern::irrefutable),
562 Pattern::At { inner, .. } => inner.irrefutable(),
563 Pattern::Const(_) | Pattern::Ctor { .. } => false,
564 }
565 }
566}
567
568#[derive(Clone, Debug)]
569pub enum CoreKind {
570 Const(Const),
571 Var(VarId),
572 /// A reference to a top-level definition.
573 Global(Arc<str>),
574 Lam {
575 /// Shared for the same reason `body` is: evaluating a `lam` hands the list to a closure,
576 /// and a refcount bump is cheaper than copying it once per call.
577 params: Arc<[VarId]>,
578 /// Shared, not owned: a closure is built every time a `lam` node is *evaluated*, and a
579 /// `Box` meant deep-copying the whole function body each time. `docs/70` §70.3 measured
580 /// 20,000 calls to a function whose executed path never changed costing 42 ms, 227 ms and
581 /// 606 ms as the *unexecuted* part of its body grew.
582 body: Arc<Core>,
583 },
584 App {
585 func: Box<Core>,
586 args: Vec<Core>,
587 },
588 Prim {
589 op: Prim,
590 args: Vec<Core>,
591 },
592 Let {
593 var: VarId,
594 value: Box<Core>,
595 body: Box<Core>,
596 },
597 If {
598 cond: Box<Core>,
599 then: Box<Core>,
600 alt: Box<Core>,
601 },
602 Match {
603 scrutinee: Box<Core>,
604 arms: Vec<Arm>,
605 },
606 /// Construct a union variant or a model record.
607 Make {
608 ty: Arc<str>,
609 variant: Option<Arc<str>>,
610 fields: Vec<(Arc<str>, Core)>,
611 },
612 Field {
613 base: Box<Core>,
614 name: Arc<str>,
615 },
616 /// `t.with(done=not t.done)` — a functional record update. The sketch's
617 /// `(set t :done (not t.done))`, and the reason `Todo` never needs a mutable binding.
618 With {
619 base: Box<Core>,
620 fields: Vec<(Arc<str>, Core)>,
621 },
622 ListLit(Vec<Core>),
623 MapLit(Vec<(Core, Core)>),
624}
625
626#[derive(Clone, Debug)]
627pub struct Core {
628 pub kind: CoreKind,
629 pub ty: Ty,
630 /// Which tier this node runs on. §4.2: "explicit tier annotation per node".
631 pub tier: Tier,
632 pub span: Span,
633 /// Set on a [`CoreKind::Var`] whose value this expression is the **last** reader of, so a
634 /// backend may move the binding rather than copy it. [`crate::liveness`] is what sets it and
635 /// what the guarantee means; `false` is always safe.
636 pub last_use: bool,
637 /// Set on a [`CoreKind::Make`]: which written field belongs at each position of the record it
638 /// builds, packed four bits per field. [`crate::fields`] is what sets it and what the packing
639 /// means; [`crate::fields::UNORDERED`] is always safe and means "sort at run time".
640 ///
641 /// It costs nothing: a `u32` here fits in the padding `last_use` already leaves, so `Core` is
642 /// 160 bytes either way.
643 pub order: u32,
644 /// Set on a [`CoreKind::Lam`]: how many bindings its body makes, so a call can reserve room
645 /// for them in one frame instead of allocating a scope per `let`. [`crate::frames`] is what
646 /// sets it and what the count means; `0` is always safe and means "chain a scope, as before".
647 pub locals: u32,
648}
649
650/// The string this expression *is*, when it is written as one.
651///
652/// Deliberately not an evaluator and deliberately not a constant folder: the one caller is the
653/// host of an outbound call, and "the host is written at the call site" is the rule that makes an
654/// egress policy derivable. A `let` that happens to bind a literal is not written at the call
655/// site, and reading through one would make the rule depend on how much folding the compiler
656/// currently does.
657pub fn literal_str(c: &Core) -> Option<Arc<str>> {
658 match &c.kind {
659 CoreKind::Const(Const::Str(s)) => Some(s.clone()),
660 _ => None,
661 }
662}
663
664impl Core {
665 pub fn new(kind: CoreKind, ty: Ty, span: Span) -> Core {
666 Core {
667 kind,
668 ty,
669 tier: Tier::Any,
670 span,
671 last_use: false,
672 order: crate::fields::UNORDERED,
673 locals: 0,
674 }
675 }
676
677 /// Every effect this expression can perform, by walking what it calls.
678 ///
679 /// Phase 1 used this as *the* effect analysis. Phase 2 does not: rows are inferred during
680 /// checking, where a call's latent row is known and a mere *reference* to a function performs
681 /// nothing. What survives is this syntactic over-approximation, used in one place where that is
682 /// the right answer — asking what a fold's function body could reach, including through a
683 /// function value it was handed.
684 pub fn effects(&self, globals: &dyn Fn(&str) -> Vec<Effect>, out: &mut Vec<Effect>) {
685 match &self.kind {
686 CoreKind::Prim { op, args } => {
687 for e in op.effects() {
688 if !out.contains(&e) {
689 out.push(e);
690 }
691 }
692 // `http_fetch`'s host is its first argument and a literal, so this walk can read
693 // it. It matters here rather than only in the checker because this is the oracle
694 // `testing::performs_itself` asks — a definition that makes the call is the one a
695 // `stub net.out(host)` replaces, and a definition that merely calls it is not.
696 if let (Prim::HttpFetch, Some(host)) = (op, args.first().and_then(literal_str)) {
697 let atom = Effect::NetOut(host);
698 if !out.contains(&atom) {
699 out.push(atom);
700 }
701 }
702 for a in args {
703 a.effects(globals, out);
704 }
705 }
706 CoreKind::Global(name) => {
707 for e in globals(name) {
708 if !out.contains(&e) {
709 out.push(e);
710 }
711 }
712 }
713 CoreKind::Const(_) | CoreKind::Var(_) => {}
714 CoreKind::Lam { body, .. } => body.effects(globals, out),
715 CoreKind::App { func, args } => {
716 func.effects(globals, out);
717 for a in args {
718 a.effects(globals, out);
719 }
720 }
721 CoreKind::Let { value, body, .. } => {
722 value.effects(globals, out);
723 body.effects(globals, out);
724 }
725 CoreKind::If { cond, then, alt } => {
726 cond.effects(globals, out);
727 then.effects(globals, out);
728 alt.effects(globals, out);
729 }
730 CoreKind::Match { scrutinee, arms } => {
731 scrutinee.effects(globals, out);
732 for e in arms.iter().flat_map(|a| a.exprs()) {
733 e.effects(globals, out);
734 }
735 }
736 CoreKind::Make { fields, .. } => {
737 for (_, f) in fields {
738 f.effects(globals, out);
739 }
740 }
741 CoreKind::Field { base, .. } => base.effects(globals, out),
742 CoreKind::With { base, fields } => {
743 base.effects(globals, out);
744 for (_, f) in fields {
745 f.effects(globals, out);
746 }
747 }
748 CoreKind::ListLit(xs) => {
749 for x in xs {
750 x.effects(globals, out);
751 }
752 }
753 CoreKind::MapLit(kvs) => {
754 for (k, v) in kvs {
755 k.effects(globals, out);
756 v.effects(globals, out);
757 }
758 }
759 }
760 }
761
762 /// Set the tier on this node and everything under it.
763 pub fn place(&mut self, tier: Tier) {
764 self.tier = tier;
765 match &mut self.kind {
766 CoreKind::Lam { body, .. } => Arc::make_mut(body).place(tier),
767 CoreKind::App { func, args } => {
768 func.place(tier);
769 for a in args {
770 a.place(tier);
771 }
772 }
773 CoreKind::Prim { args, .. } => {
774 for a in args {
775 a.place(tier);
776 }
777 }
778 CoreKind::Let { value, body, .. } => {
779 value.place(tier);
780 body.place(tier);
781 }
782 CoreKind::If { cond, then, alt } => {
783 cond.place(tier);
784 then.place(tier);
785 alt.place(tier);
786 }
787 CoreKind::Match { scrutinee, arms } => {
788 scrutinee.place(tier);
789 for e in arms.iter_mut().flat_map(|a| a.exprs_mut()) {
790 e.place(tier);
791 }
792 }
793 CoreKind::Make { fields, .. } => {
794 for (_, f) in fields {
795 f.place(tier);
796 }
797 }
798 CoreKind::Field { base, .. } => base.place(tier),
799 CoreKind::With { base, fields } => {
800 base.place(tier);
801 for (_, f) in fields {
802 f.place(tier);
803 }
804 }
805 CoreKind::ListLit(xs) => {
806 for x in xs {
807 x.place(tier);
808 }
809 }
810 CoreKind::MapLit(kvs) => {
811 for (k, v) in kvs {
812 k.place(tier);
813 v.place(tier);
814 }
815 }
816 CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => {}
817 }
818 }
819}
820
821// ---------------------------------------------------------------------------------------------
822// Values
823// ---------------------------------------------------------------------------------------------
824
825/// A runtime value.
826///
827/// `Map` and a record's fields are both *ordered* on purpose and for the same reason Phase 0 chose
828/// `BTreeMap`: iteration order is part of the rendered view, and replay must reproduce the *patch
829/// stream* bit for bit, not merely the set of values.
830///
831/// `Map` is a [`PMap`], not an `Arc<BTreeMap>`, because it is the fold's accumulator: an update
832/// must not copy it. See [`crate::pmap`] for why that structure and not another.
833#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
834pub enum Value {
835 Unit,
836 Bool(bool),
837 Int(i64),
838 /// A real, stored as an **order-preserving** key rather than as `f64::to_bits`, so that the
839 /// derived `Ord` is the numeric one.
840 ///
841 /// A total order is not optional here — a map key and a component of the state digest need one
842 /// — and `to_bits` supplies one that disagrees with arithmetic: `-1.0` has a larger bit pattern
843 /// than `1.0`, so `<` answered backwards for every negative number and `sort_by` sorted the
844 /// negatives in reverse. [`Value::float`] applies the standard monotone transform instead
845 /// (flip the sign bit for a positive, invert every bit for a negative), which makes the two
846 /// orders the same order. `docs/27` §27.8.
847 Float(u64),
848 /// Text, with the two facts a character-indexed language needs about it: how many characters
849 /// there are, and whether a character index is a byte index. [`Text`] is why.
850 Str(Arc<Text>),
851 /// A list, in one of [`crate::seq::Seq`]'s two layouts.
852 ///
853 /// Behind the `Arc` rather than in the `Value`, which is what keeps a `Value` 16 bytes: the
854 /// layout enum costs a word inside the allocation a list already had, and nothing that is not
855 /// a list pays for it.
856 List(Arc<Seq>),
857 Map(PMap<Value, Value>),
858 /// A model instance or a union variant — see [`Record`].
859 ///
860 /// Behind one pointer rather than inline, and that is a size decision rather than a style one:
861 /// the three fields inline made **every** `Value` 48 bytes, so a list of a million integers
862 /// carried 32 bytes of nothing per element and a call frame paid for the widest variant it did
863 /// not hold. One `Arc` makes a `Value` 16.
864 Data(Arc<Record>),
865 Html(Arc<Html>),
866 /// An attribute waiting to be attached to an element.
867 Attr(Arc<AttrValue>),
868 Closure(Arc<Closure>),
869}
870
871/// A model instance or a union variant. `variant` is `None` for a plain record.
872///
873/// Split out of [`Value::Data`] so that a `Value` is a discriminant and a pointer. Records are the
874/// widest thing the language has and the rarest thing in a hot loop, which is exactly the shape
875/// that should be behind an indirection.
876#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
877pub struct Record {
878 pub ty: Arc<str>,
879 pub variant: Option<Arc<str>>,
880 pub fields: Fields,
881}
882
883/// A record's fields, sorted by name.
884///
885/// This was a `BTreeMap`, and a record is the wrong size for one: three to eight entries, built
886/// once and read many times. A B-tree pays a node allocation and a pointer chase per level to buy
887/// an asymptotic advantage that never arrives at that size, and profiling `awfy/havlak.beck` put
888/// a fifth of the process inside its search, its insert and the `memcmp` underneath them.
889///
890/// Sorted by name and searched linearly: one allocation for the whole record, the names lie next
891/// to each other in cache, and `get` compares lengths before bytes because it wants equality
892/// rather than order. Iteration is in name order, so the value order, the state digest and the
893/// wire format ([`crate::repr`]) are all bit-for-bit what the `BTreeMap` gave — which is what
894/// makes this a representation change and not a semantic one.
895#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
896pub struct Fields(Vec<(Arc<str>, Value)>);
897
898impl Fields {
899 pub fn new() -> Fields {
900 Fields(Vec::new())
901 }
902
903 pub fn with_capacity(n: usize) -> Fields {
904 Fields(Vec::with_capacity(n))
905 }
906
907 pub fn get(&self, name: &str) -> Option<&Value> {
908 self.0
909 .iter()
910 .find(|(k, _)| same_name(k, name))
911 .map(|(_, v)| v)
912 }
913
914 /// Set `name`, keeping the order by name. Answers the value that was there.
915 ///
916 /// The search is by **equality** and not by order, which is the whole difference: `==` on two
917 /// `str`s compares their lengths first and reaches `memcmp` only for a pair that could match,
918 /// where a binary search has to order every probe it makes. A record has three to eight
919 /// fields, so a scan makes at most as many comparisons as a binary search and nearly all of
920 /// them are an integer test. Only a field that is genuinely new pays for the ordered insert,
921 /// and `with` — which is what calls this in a loop — never has one.
922 pub fn insert(&mut self, name: Arc<str>, value: Value) -> Option<Value> {
923 if let Some(slot) = self.0.iter_mut().find(|(k, _)| same_name(k, &name)) {
924 return Some(std::mem::replace(&mut slot.1, value));
925 }
926 let at = self
927 .0
928 .partition_point(|(k, _)| cmp_name(k, &name) == std::cmp::Ordering::Less);
929 self.0.insert(at, (name, value));
930 None
931 }
932
933 /// Build from fields in **any** order, sorting once.
934 ///
935 /// This is how a record literal is built, and it is a separate entry point from `insert` in a
936 /// loop because the two cost differently: `sort_unstable_by` on a handful of elements is an
937 /// insertion sort, which makes `n - 1` comparisons and moves nothing when the fields already
938 /// arrive in order — as a record literal's usually do.
939 pub fn from_pairs(mut pairs: Vec<(Arc<str>, Value)>) -> Fields {
940 pairs.sort_unstable_by(|(a, _), (b, _)| cmp_name(a, b));
941 pairs.dedup_by(|(a, _), (b, _)| same_name(a, b));
942 Fields(pairs)
943 }
944
945 /// Build from fields the caller has **already** put in order.
946 ///
947 /// The caller is [`crate::fields`], which decided the order once at compile time — a record
948 /// literal's field names are written in the source, so sorting them once per record built is
949 /// work with a known answer. Nothing else should use this: the order is the `Map` iteration,
950 /// the state digest and the patch stream, so getting it wrong is a wire-format bug rather than
951 /// a slow lookup.
952 pub fn from_sorted(pairs: Vec<(Arc<str>, Value)>) -> Fields {
953 debug_assert!(
954 pairs
955 .windows(2)
956 .all(|w| cmp_name(&w[0].0, &w[1].0) == std::cmp::Ordering::Less),
957 "from_sorted was given fields that are not sorted and distinct"
958 );
959 Fields(pairs)
960 }
961
962 pub fn len(&self) -> usize {
963 self.0.len()
964 }
965
966 pub fn is_empty(&self) -> bool {
967 self.0.is_empty()
968 }
969
970 pub fn iter(&self) -> std::slice::Iter<'_, (Arc<str>, Value)> {
971 self.0.iter()
972 }
973
974 pub fn values(&self) -> impl Iterator<Item = &Value> {
975 self.0.iter().map(|(_, v)| v)
976 }
977}
978
979impl FromIterator<(Arc<str>, Value)> for Fields {
980 fn from_iter<I: IntoIterator<Item = (Arc<str>, Value)>>(it: I) -> Fields {
981 Fields::from_pairs(it.into_iter().collect())
982 }
983}
984
985impl<'a> IntoIterator for &'a Fields {
986 type Item = &'a (Arc<str>, Value);
987 type IntoIter = std::slice::Iter<'a, (Arc<str>, Value)>;
988 fn into_iter(self) -> Self::IntoIter {
989 self.0.iter()
990 }
991}
992
993/// A string that knows its own length in **characters**, and whether it is ASCII.
994///
995/// Beck indexes text by character everywhere or nowhere (`docs/46` §46.6), and a `String` counts
996/// bytes — so `str_len` used to be `chars().count()` and `str_slice` used to `skip()` its way to
997/// the start. Both are `O(n)` in the *string* rather than in the answer, which makes the ordinary
998/// way to walk one — `while i < str_len(s)` reading `str_slice(s, i, 1)` — quadratic. Measured at
999/// ×2.7 per doubling in [`70`](../../../../../docs/70-the-evaluator-gets-fast-report.md) §70.2.
1000///
1001/// Both facts are computed once, when the string is built, which is work the construction was
1002/// already doing: it had to copy the bytes, and `is_ascii` is a scan of the same bytes that answers
1003/// `chars` for free when it is true. Everything downstream is then `O(1)` or `O(answer)`.
1004///
1005/// The `String` rather than a `Box<str>` is the other half: it has spare capacity, so `a + b` can
1006/// push into `a` when the last-use analysis proves nobody else holds it ([`crate::liveness`]).
1007#[derive(Clone, Debug)]
1008pub struct Text {
1009 bytes: String,
1010 /// Characters, not bytes. `str_len`'s answer.
1011 chars: usize,
1012 /// Every character is one byte, so character index == byte index and a slice is a byte range.
1013 ascii: bool,
1014 /// For text that is *not* ASCII: the byte offset of every 32nd character.
1015 ///
1016 /// Chunked rather than one entry per character, because the point is to stop paying `O(n)` per
1017 /// slice and a jump to the nearest 32 does that for a thirty-second of the memory — `n / 8`
1018 /// bytes, and only for text that needs it, since an ASCII character index *is* a byte index.
1019 ///
1020 /// Built eagerly, in the pass that counts the characters, rather than cached on first use. A
1021 /// lazy one would be interior mutability inside a `Value`, and a `Value` is a `Map` key: the
1022 /// cache would be invisible to `Ord` and `Hash` and therefore harmless, but "harmless interior
1023 /// mutability in a key" is a sentence every reader and `clippy::mutable_key_type` would have to
1024 /// re-check. One pass and an eighth of the bytes is the cheaper answer.
1025 index: Box<[u32]>,
1026}
1027
1028/// How many characters one entry of [`Text`]'s index skips.
1029const INDEX_STRIDE: usize = 32;
1030
1031impl Text {
1032 pub fn new(bytes: String) -> Text {
1033 let ascii = bytes.is_ascii();
1034 // ASCII answers both questions from the scan `is_ascii` already did, and needs no index at
1035 // all. Anything else pays one more pass — once, here — rather than paying it on every
1036 // `str_len` and every `str_slice`.
1037 if ascii {
1038 let chars = bytes.len();
1039 return Text {
1040 bytes,
1041 chars,
1042 ascii,
1043 index: Box::new([]),
1044 };
1045 }
1046 let mut chars = 0usize;
1047 let mut index = Vec::with_capacity(bytes.len() / INDEX_STRIDE + 1);
1048 for (at, _) in bytes.char_indices() {
1049 if chars.is_multiple_of(INDEX_STRIDE) {
1050 index.push(at as u32);
1051 }
1052 chars += 1;
1053 }
1054 Text {
1055 bytes,
1056 chars,
1057 ascii,
1058 index: index.into_boxed_slice(),
1059 }
1060 }
1061
1062 /// The byte offset of character `i`, in constant time for ASCII text and in at most
1063 /// one index stride for anything else. Past the end it answers the end, which is what a
1064 /// clamping slice wants.
1065 pub fn byte_offset(&self, i: usize) -> usize {
1066 if self.ascii {
1067 return i.min(self.bytes.len());
1068 }
1069 if i >= self.chars {
1070 return self.bytes.len();
1071 }
1072 let chunk = i / INDEX_STRIDE;
1073 let from = self.index.get(chunk).copied().unwrap_or(0) as usize;
1074 match self.bytes[from..]
1075 .char_indices()
1076 .nth(i - chunk * INDEX_STRIDE)
1077 {
1078 Some((at, _)) => from + at,
1079 None => self.bytes.len(),
1080 }
1081 }
1082
1083 /// The length in characters, in constant time.
1084 pub fn chars_len(&self) -> usize {
1085 self.chars
1086 }
1087
1088 pub fn as_str(&self) -> &str {
1089 &self.bytes
1090 }
1091
1092 /// Append, consuming: the caller has established sole ownership, so this is a `push_str` and
1093 /// not a copy of both sides.
1094 pub fn appended(mut self, other: &str) -> Text {
1095 let before = self.chars;
1096 let start = self.bytes.len();
1097 let other_ascii = other.is_ascii();
1098 self.bytes.push_str(other);
1099
1100 // Everything here is `O(other)`, never `O(self)`, which is the property the whole change
1101 // exists for: appending in a loop has to stay linear in the total.
1102 if self.ascii && other_ascii {
1103 self.chars = self.bytes.len();
1104 return self;
1105 }
1106 if self.ascii {
1107 // The left half was ASCII, so its character numbers *are* its byte offsets and its
1108 // share of the index can be written down rather than walked for.
1109 let mut index = Vec::with_capacity(self.bytes.len() / INDEX_STRIDE + 1);
1110 let mut at = 0;
1111 while at < before {
1112 index.push(at as u32);
1113 at += INDEX_STRIDE;
1114 }
1115 self.index = index.into_boxed_slice();
1116 self.ascii = false;
1117 }
1118 let mut index = std::mem::take(&mut self.index).into_vec();
1119 let mut chars = before;
1120 for (at, _) in self.bytes[start..].char_indices() {
1121 if chars.is_multiple_of(INDEX_STRIDE) {
1122 index.push((start + at) as u32);
1123 }
1124 chars += 1;
1125 }
1126 self.chars = chars;
1127 self.index = index.into_boxed_slice();
1128 self
1129 }
1130}
1131
1132impl std::ops::Deref for Text {
1133 type Target = str;
1134 fn deref(&self) -> &str {
1135 &self.bytes
1136 }
1137}
1138
1139impl From<&str> for Text {
1140 fn from(s: &str) -> Text {
1141 Text::new(s.to_string())
1142 }
1143}
1144
1145/// Text compares, orders and hashes **as its characters**, so that adding the two cached facts
1146/// cannot change what a program means: a `Map` keyed by strings keeps its order, and so does the
1147/// state digest that a replay has to reproduce.
1148impl PartialEq for Text {
1149 fn eq(&self, other: &Self) -> bool {
1150 self.bytes == other.bytes
1151 }
1152}
1153impl Eq for Text {}
1154impl PartialOrd for Text {
1155 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1156 Some(self.cmp(other))
1157 }
1158}
1159impl Ord for Text {
1160 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1161 self.bytes.cmp(&other.bytes)
1162 }
1163}
1164impl std::hash::Hash for Text {
1165 fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
1166 self.bytes.hash(h)
1167 }
1168}
1169
1170#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1171pub enum AttrValue {
1172 Plain(Arc<str>, Arc<str>),
1173 On(Arc<str>, Value),
1174 Key(Arc<str>),
1175}
1176
1177#[derive(Debug)]
1178pub struct Closure {
1179 pub params: Arc<[VarId]>,
1180 /// The same `Arc` the [`CoreKind::Lam`] node holds, so building a closure is a refcount bump
1181 /// rather than a copy of the code.
1182 pub body: Arc<Core>,
1183 /// Behind an `Arc` so that *calling* the closure clones a pointer rather than the environment.
1184 pub env: Arc<Env>,
1185 /// How many bindings the body makes, copied off the [`CoreKind::Lam`] node so that a call can
1186 /// size one frame for the parameters and all of them. [`crate::frames`] is what counts it.
1187 pub locals: u32,
1188}
1189
1190impl PartialEq for Closure {
1191 /// Closures compare by identity of their code position. Two closures are never equal unless
1192 /// they came from the same lambda with the same captured frame, which is all any program
1193 /// should rely on.
1194 fn eq(&self, other: &Self) -> bool {
1195 self.params == other.params
1196 && self.body.span == other.body.span
1197 && self.env.frame == other.env.frame
1198 }
1199}
1200impl Eq for Closure {}
1201impl PartialOrd for Closure {
1202 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1203 Some(self.cmp(other))
1204 }
1205}
1206impl Ord for Closure {
1207 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1208 self.params
1209 .cmp(&other.params)
1210 .then_with(|| self.body.span.start.cmp(&other.body.span.start))
1211 }
1212}
1213
1214/// A lexical environment: a persistent chain of frames, so a closure can capture cheaply.
1215#[derive(Clone, Debug, Default, PartialEq, Eq)]
1216pub struct Env {
1217 /// `Arc<[T]>` rather than `Arc<Vec<T>>`: the second is two allocations and two hops to reach a
1218 /// binding.
1219 ///
1220 /// A call sizes this for the parameters **and** for every binding the body will make, so a
1221 /// `let` writes into a slot that is already there rather than allocating a scope of its own.
1222 /// The unwritten tail is filled with [`TOMBSTONE`], which no variable is named.
1223 frame: Arc<[(VarId, Value)]>,
1224 /// How much of `frame` holds a binding. Everything from here up is reserved and empty.
1225 used: u32,
1226 parent: Option<Arc<Env>>,
1227}
1228
1229impl Env {
1230 pub fn new() -> Env {
1231 Env::default()
1232 }
1233
1234 pub fn extend(&self, bindings: Vec<(VarId, Value)>) -> Env {
1235 Env {
1236 used: bindings.len() as u32,
1237 frame: bindings.into(),
1238 parent: Some(Arc::new(self.clone())),
1239 }
1240 }
1241
1242 /// The frame a call runs in: the parameters, then `locals` reserved slots for the bindings the
1243 /// body is going to make.
1244 ///
1245 /// One allocation. `Map<Range, _>` has a length the compiler can trust, so collecting it into
1246 /// an `Arc<[_]>` sizes the allocation once — which is why the parameters and the reserved tail
1247 /// are produced by one iterator rather than a vector that is then converted.
1248 pub fn call_frame(
1249 parent: &Arc<Env>,
1250 params: &[VarId],
1251 args: impl Iterator<Item = Value>,
1252 locals: u32,
1253 ) -> Env {
1254 let n = params.len();
1255 let mut given = args;
1256 let frame = (0..n + locals as usize)
1257 .map(|i| match given.next() {
1258 Some(v) => (params[i], v),
1259 None => (TOMBSTONE, Value::Unit),
1260 })
1261 .collect();
1262 Env {
1263 frame,
1264 used: n as u32,
1265 parent: Some(Arc::clone(parent)),
1266 }
1267 }
1268
1269 /// Bind `bindings` into this frame's reserved tail, if there is room for all of them and
1270 /// nobody else is holding the frame.
1271 ///
1272 /// Answers whether it did. `false` means the caller must fall back to [`Env::extend`] and
1273 /// chain a scope — which happens when a closure has captured this environment (its clone holds
1274 /// the frame, so `Arc::get_mut` refuses), when the reservation was too small, or when the
1275 /// program was built by something that never ran the reservation pass at all.
1276 ///
1277 /// The safety argument is the refusal: a closure that captured this environment can see the
1278 /// slots this would write, and `Arc::get_mut` is what proves that has not happened. Every
1279 /// binding gets a slot of its own, so nothing a closure captured is ever overwritten.
1280 pub fn put(&mut self, bindings: &mut Vec<(VarId, Value)>) -> bool {
1281 let at = self.used as usize;
1282 let n = bindings.len();
1283 if at + n > self.frame.len() {
1284 return false;
1285 }
1286 let Some(frame) = Arc::get_mut(&mut self.frame) else {
1287 return false;
1288 };
1289 for (i, b) in bindings.drain(..).enumerate() {
1290 frame[at + i] = b;
1291 }
1292 self.used = (at + n) as u32;
1293 true
1294 }
1295
1296 /// [`Env::put`] for a single binding, which is what a `let` is — and a `let` is much the most
1297 /// common of the two, so it does not build a vector to hand over. Answers the value back when
1298 /// there is no room for it.
1299 pub fn put_one(&mut self, var: VarId, value: Value) -> Result<(), Value> {
1300 let at = self.used as usize;
1301 if at >= self.frame.len() {
1302 return Err(value);
1303 }
1304 match Arc::get_mut(&mut self.frame) {
1305 Some(frame) => {
1306 frame[at] = (var, value);
1307 self.used += 1;
1308 Ok(())
1309 }
1310 None => Err(value),
1311 }
1312 }
1313
1314 /// The part of the frame that holds bindings, without the reserved tail.
1315 #[inline]
1316 fn bound(&self) -> &[(VarId, Value)] {
1317 &self.frame[..self.used as usize]
1318 }
1319
1320 /// An environment with nothing in it, behind an `Arc`, shared by every top-level definition.
1321 pub fn empty_shared() -> Arc<Env> {
1322 Arc::new(Env::new())
1323 }
1324
1325 pub fn get(&self, v: VarId) -> Option<&Value> {
1326 let mut env = self;
1327 loop {
1328 if let Some((_, value)) = env.bound().iter().rev().find(|(id, _)| *id == v) {
1329 return Some(value);
1330 }
1331 match &env.parent {
1332 Some(p) => env = p,
1333 None => return None,
1334 }
1335 }
1336 }
1337
1338 /// Read `v`, and **move** it out of the frame when three things hold: the caller says no later
1339 /// evaluation reads it, this environment is the only holder of the frame it lives in, and the
1340 /// value is one whose copy costs something.
1341 ///
1342 /// The third condition is not an optimisation of an optimisation — it is what keeps the other
1343 /// two from costing more than they save. Moving is strictly more work than cloning at the point
1344 /// of the read: a clone of an `Int` is a copy of eight bytes and a clone of a container is one
1345 /// atomic increment, where a move has to find the slot, prove the frame is unshared and empty
1346 /// it. It pays only when somebody downstream can then *use* the sole ownership — which today is
1347 /// `list_append` pushing in place and `with` rebuilding a record's fields — and measuring it
1348 /// without this condition showed every benchmark in the tree 6–13% slower, because the reads
1349 /// that dominate a real program are of `Int`s and nothing was gained by moving one
1350 /// ([`docs/46`](../../../../../docs/46-standard-library-report.md) §46.14).
1351 ///
1352 /// The caller must have established that no later evaluation reads `v` — [`crate::liveness`]
1353 /// is what establishes it, and `last_use` is the flag. What this adds is the second half of the
1354 /// safety argument: a frame is emptied only when nothing else holds it, so an environment
1355 /// captured by a closure or shared with an inner scope is read from rather than emptied, and a
1356 /// caller that is wrong about liveness gets an unbound-variable error rather than somebody
1357 /// else's missing binding.
1358 pub fn read(&mut self, v: VarId, may_move: bool) -> Option<Value> {
1359 // The overwhelmingly common read is not a last use, and it needs none of the machinery
1360 // below: no scope on the way to the binding has to be proved unshared, because nothing is
1361 // going to be taken out of one. That proof costs **two atomic loads per scope level**
1362 // (`strong_count` and `weak_count`) plus an `Arc::get_mut`, and it was being paid on every
1363 // variable a program reads. `get` is a plain walk.
1364 if !may_move {
1365 return self.get(v).cloned();
1366 }
1367 let mut env = self;
1368 loop {
1369 if let Some(i) = env.bound().iter().rposition(|(id, _)| *id == v) {
1370 if may_move && worth_moving(&env.frame[i].1) {
1371 if let Some(frame) = Arc::get_mut(&mut env.frame) {
1372 // Tombstoned rather than removed: `Vec::remove` shifts every binding above
1373 // it, and this runs on the hottest path there is. The slot keeps its place
1374 // under a name no variable has, so a later read of `v` misses it and says
1375 // so instead of finding a neighbour.
1376 frame[i].0 = TOMBSTONE;
1377 return Some(std::mem::replace(&mut frame[i].1, Value::Unit));
1378 }
1379 }
1380 return Some(env.frame[i].1.clone());
1381 }
1382 let shared_parent = env
1383 .parent
1384 .as_ref()
1385 .is_some_and(|p| Arc::strong_count(p) > 1 || Arc::weak_count(p) > 0);
1386 if shared_parent {
1387 return env.parent.as_ref().and_then(|p| p.get(v)).cloned();
1388 }
1389 match env.parent.as_mut() {
1390 Some(p) => match Arc::get_mut(p) {
1391 Some(p) => env = p,
1392 None => return None,
1393 },
1394 None => return None,
1395 }
1396 }
1397 }
1398}
1399
1400/// Every subexpression of this one.
1401///
1402/// The read-only twin of `children_mut`, and it exists for the same reason: a pass that walks
1403/// the whole tree should not restate the shape of `CoreKind`, because the day a variant gains a
1404/// child every hand-written walk is silently incomplete ([`docs/90`](../../../../../docs/90-pattern-matching-report.md)
1405/// §90.5 is that failure, at fourteen sites).
1406pub fn children(c: &Core) -> Vec<&Core> {
1407 match &c.kind {
1408 CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => Vec::new(),
1409 CoreKind::Lam { body, .. } => vec![body],
1410 CoreKind::App { func, args } => std::iter::once(&**func).chain(args).collect(),
1411 CoreKind::Let { value, body, .. } => vec![value, body],
1412 CoreKind::If { cond, then, alt } => vec![cond, then, alt],
1413 CoreKind::Match { scrutinee, arms } => std::iter::once(&**scrutinee)
1414 .chain(arms.iter().flat_map(|a| a.exprs()))
1415 .collect(),
1416 CoreKind::Prim { args, .. } => args.iter().collect(),
1417 CoreKind::Make { fields, .. } => fields.iter().map(|(_, f)| f).collect(),
1418 CoreKind::Field { base, .. } => vec![base],
1419 CoreKind::With { base, fields } => std::iter::once(&**base)
1420 .chain(fields.iter().map(|(_, f)| f))
1421 .collect(),
1422 CoreKind::ListLit(items) => items.iter().collect(),
1423 CoreKind::MapLit(kvs) => kvs.iter().flat_map(|(k, v)| [k, v]).collect(),
1424 }
1425}
1426
1427/// Every subexpression of this one, to be rewritten in place.
1428///
1429/// The walk two passes over the finished program share — [`crate::frames`] and [`crate::fields`].
1430/// A lambda's body is behind an `Arc` because a closure shares it rather than copying it
1431/// (`docs/70`), so reaching into one is a `make_mut`: it runs once, on a program nothing else
1432/// holds yet, so nothing is actually cloned.
1433pub(crate) fn children_mut(c: &mut Core) -> Vec<&mut Core> {
1434 match &mut c.kind {
1435 CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => Vec::new(),
1436 CoreKind::Lam { body, .. } => vec![Arc::make_mut(body)],
1437 CoreKind::App { func, args } => std::iter::once(&mut **func).chain(args).collect(),
1438 CoreKind::Let { value, body, .. } => vec![&mut **value, &mut **body],
1439 CoreKind::If { cond, then, alt } => vec![&mut **cond, &mut **then, &mut **alt],
1440 CoreKind::Match { scrutinee, arms } => std::iter::once(&mut **scrutinee)
1441 .chain(arms.iter_mut().flat_map(|a| a.exprs_mut()))
1442 .collect(),
1443 CoreKind::Prim { args, .. } => args.iter_mut().collect(),
1444 CoreKind::Make { fields, .. } => fields.iter_mut().map(|(_, f)| f).collect(),
1445 CoreKind::Field { base, .. } => vec![&mut **base],
1446 CoreKind::With { base, fields } => std::iter::once(&mut **base)
1447 .chain(fields.iter_mut().map(|(_, f)| f))
1448 .collect(),
1449 CoreKind::ListLit(items) => items.iter_mut().collect(),
1450 CoreKind::MapLit(kvs) => kvs.iter_mut().flat_map(|(k, v)| [k, v]).collect(),
1451 }
1452}
1453
1454/// Are these the same field name?
1455///
1456/// Length, then first byte, then the rest. `str`'s own `==` checks the length and hands the bytes
1457/// to `memcmp`, and a `memcmp` call is dear next to what it decides here: field names are short,
1458/// there are three to eight of them in a record, and two that share a length almost never share a
1459/// first letter. Profiling `awfy/richards.beck` put 6% of the process inside `memcmp`, nearly all
1460/// of it deciding between `kind` and `link`.
1461#[inline]
1462fn same_name(a: &str, b: &str) -> bool {
1463 let (x, y) = (a.as_bytes(), b.as_bytes());
1464 x.len() == y.len() && (x.is_empty() || x[0] == y[0]) && x == y
1465}
1466
1467/// The order a record keeps its fields in.
1468///
1469/// [`Fields`] is sorted by name and that order is load-bearing far outside this module — it is a
1470/// `Map`'s iteration, the state digest, the patch stream and the order `Ord` compares two records
1471/// in. A backend that lays a record out in memory has to lay it out in *this* order or its `<`
1472/// disagrees with the evaluator's, so the order is published rather than reimplemented.
1473pub fn field_order(a: &str, b: &str) -> std::cmp::Ordering {
1474 cmp_name(a, b)
1475}
1476
1477/// Order two field names, deciding on the first byte where it can.
1478///
1479/// `<[u8]>::cmp` calls `memcmp` over the common prefix before it looks at the lengths, so it pays
1480/// a call to distinguish `id` from `kind`. Sorting a record's fields is the other half of what
1481/// `same_name` documents.
1482#[inline]
1483pub(crate) fn cmp_name(a: &str, b: &str) -> std::cmp::Ordering {
1484 let (x, y) = (a.as_bytes(), b.as_bytes());
1485 match (x.first(), y.first()) {
1486 (Some(p), Some(q)) if p != q => p.cmp(q),
1487 _ => x.cmp(y),
1488 }
1489}
1490
1491/// The name a moved-out binding takes, which no variable has: `VarId`s are handed out from zero by
1492/// the checker, and a program with four billion of them has lost to `MAX_NESTING` long before.
1493const TOMBSTONE: VarId = VarId::MAX;
1494
1495/// Whether moving this value out of a frame can save anything downstream.
1496///
1497/// A `List` can be pushed into by `list_append`, a `Str` by `+`, and a record's fields can be
1498/// rebuilt in place by `with` — each only when nobody else holds them. Everything else is either a copy of a few bytes
1499/// or an atomic increment, and moving it costs more than it saves — `Env::read` is the measurement.
1500fn worth_moving(v: &Value) -> bool {
1501 matches!(v, Value::List(_) | Value::Str(_) | Value::Data(_))
1502}
1503
1504/// The monotone `f64` → `u64` transform: for a non-negative float flip the sign bit, for a
1505/// negative one invert every bit. `a < b` as reals iff `order_key(a) < order_key(b)` as integers,
1506/// with `-inf` at the bottom and NaN above `+inf`.
1507const SIGN: u64 = 1 << 63;
1508
1509fn order_key(f: f64) -> u64 {
1510 let bits = f.to_bits();
1511 if bits & SIGN != 0 {
1512 !bits
1513 } else {
1514 bits ^ SIGN
1515 }
1516}
1517
1518fn from_order_key(key: u64) -> f64 {
1519 f64::from_bits(if key & SIGN != 0 { key ^ SIGN } else { !key })
1520}
1521
1522impl Value {
1523 /// Make a real, canonicalising the two IEEE values that would otherwise break `Eq`.
1524 ///
1525 /// `-0.0` becomes `0.0` and every NaN becomes one NaN, because [`Value`] is `Eq` and `Ord` and
1526 /// a fold's accumulator is compared, hashed and used as a map key. IEEE 754 says `NaN != NaN`
1527 /// and `-0.0 == 0.0`; both are irreconcilable with a total order, and the total order is the
1528 /// one §3.7 needs. So Beck's `==` on reals is *structural*, and `docs/27` §27.8 says so where
1529 /// somebody porting numeric code will read it.
1530 pub fn float(f: f64) -> Value {
1531 let f = if f.is_nan() {
1532 f64::NAN
1533 } else if f == 0.0 {
1534 0.0
1535 } else {
1536 f
1537 };
1538 Value::Float(order_key(f))
1539 }
1540
1541 pub fn as_f64(&self) -> Option<f64> {
1542 match self {
1543 Value::Float(key) => Some(from_order_key(*key)),
1544 _ => None,
1545 }
1546 }
1547
1548 pub fn str_(s: impl AsRef<str>) -> Value {
1549 Value::Str(Arc::new(Text::from(s.as_ref())))
1550 }
1551
1552 /// The same from a `String` that is already owned, which is most of the string primitives:
1553 /// they build one and would otherwise copy it again on the way in.
1554 pub fn text(s: String) -> Value {
1555 Value::Str(Arc::new(Text::new(s)))
1556 }
1557
1558 pub fn as_str(&self) -> Option<&str> {
1559 match self {
1560 Value::Str(s) => Some(s),
1561 _ => None,
1562 }
1563 }
1564
1565 pub fn as_int(&self) -> Option<i64> {
1566 match self {
1567 Value::Int(i) => Some(*i),
1568 _ => None,
1569 }
1570 }
1571
1572 pub fn as_bool(&self) -> Option<bool> {
1573 match self {
1574 Value::Bool(b) => Some(*b),
1575 _ => None,
1576 }
1577 }
1578
1579 /// A list of these elements, in whatever layout they fit ([`crate::seq::Seq::pack`]).
1580 ///
1581 /// The one constructor, so that the choice of layout is made in one place rather than at
1582 /// seventy call sites — and so that turning it off turns it off everywhere.
1583 pub fn list(values: Vec<Value>) -> Value {
1584 Value::List(Arc::new(Seq::pack(values)))
1585 }
1586
1587 /// A list of elements already in a layout — what a primitive that *preserved* one hands back.
1588 ///
1589 /// [`Value::list`] chooses a layout; this keeps the one the caller has, which is the difference
1590 /// between `list_append` on a column costing a `push` and costing a re-examination of the whole
1591 /// list.
1592 pub fn of_seq(seq: Seq) -> Value {
1593 Value::List(Arc::new(seq))
1594 }
1595
1596 pub fn as_list(&self) -> Option<&Seq> {
1597 match self {
1598 Value::List(xs) => Some(xs),
1599 _ => None,
1600 }
1601 }
1602
1603 pub fn as_map(&self) -> Option<&PMap<Value, Value>> {
1604 match self {
1605 Value::Map(m) => Some(m),
1606 _ => None,
1607 }
1608 }
1609
1610 pub fn as_html(&self) -> Option<&Html> {
1611 match self {
1612 Value::Html(h) => Some(h),
1613 _ => None,
1614 }
1615 }
1616
1617 /// Build a record or a union variant. The one constructor, so that the `Arc` and the map are
1618 /// allocated in one place rather than at every call site.
1619 pub fn data(ty: impl Into<Arc<str>>, variant: Option<Arc<str>>, fields: Fields) -> Value {
1620 Value::Data(Arc::new(Record {
1621 ty: ty.into(),
1622 variant,
1623 fields,
1624 }))
1625 }
1626
1627 /// The same from a list of pairs, which is how most call sites have them.
1628 pub fn record<const N: usize>(
1629 ty: impl Into<Arc<str>>,
1630 variant: Option<&str>,
1631 fields: [(&str, Value); N],
1632 ) -> Value {
1633 Value::data(
1634 ty,
1635 variant.map(Arc::from),
1636 fields.into_iter().map(|(k, v)| (Arc::from(k), v)).collect(),
1637 )
1638 }
1639
1640 pub fn field(&self, name: &str) -> Option<&Value> {
1641 match self {
1642 Value::Data(d) => d.fields.get(name),
1643 _ => None,
1644 }
1645 }
1646
1647 pub fn variant(&self) -> Option<&str> {
1648 match self {
1649 Value::Data(d) => d.variant.as_deref(),
1650 _ => None,
1651 }
1652 }
1653
1654 pub fn some(v: Value) -> Value {
1655 Value::record(Ty::OPTION, Some("Some"), [("value", v)])
1656 }
1657
1658 pub fn none() -> Value {
1659 Value::record(Ty::OPTION, Some("None"), [])
1660 }
1661
1662 pub fn ok(v: Value) -> Value {
1663 Value::record(Ty::RESULT, Some("Ok"), [("value", v)])
1664 }
1665
1666 pub fn err(v: Value) -> Value {
1667 Value::record(Ty::RESULT, Some("Err"), [("error", v)])
1668 }
1669
1670 /// How `str(x)` renders a value, and how a value becomes a `Map` key's printed form.
1671 pub fn display(&self) -> String {
1672 match self {
1673 Value::Unit => "unit".into(),
1674 Value::Bool(b) => b.to_string(),
1675 Value::Int(i) => i.to_string(),
1676 Value::Float(_) => format!("{}", self.as_f64().unwrap_or(0.0)),
1677 Value::Str(s) => s.to_string(),
1678 Value::List(xs) => {
1679 let mut parts: Vec<String> = Vec::with_capacity(xs.len());
1680 xs.for_each(|x| parts.push(x.display()));
1681 format!("[{}]", parts.join(", "))
1682 }
1683 Value::Map(m) => {
1684 let parts: Vec<String> = m
1685 .iter()
1686 .map(|(k, v)| format!("{}: {}", k.display(), v.display()))
1687 .collect();
1688 format!("{{{}}}", parts.join(", "))
1689 }
1690 Value::Data(d) => {
1691 // A newtype wrapping one field prints as that field — `Id(uuid)` reads as the uuid,
1692 // which is what a key attribute and a rendered list want.
1693 if d.variant.is_none() && d.fields.len() == 1 {
1694 if let Some(v) = d.fields.values().next() {
1695 return v.display();
1696 }
1697 }
1698 let name = d.variant.as_deref().unwrap_or(&d.ty);
1699 if d.fields.is_empty() {
1700 return name.to_string();
1701 }
1702 let parts: Vec<String> = d
1703 .fields
1704 .iter()
1705 .map(|(k, v)| format!("{k}: {}", v.display()))
1706 .collect();
1707 format!("{name}{{{}}}", parts.join(", "))
1708 }
1709 Value::Html(h) => h.render(),
1710 Value::Attr(_) => "<attr>".into(),
1711 Value::Closure(_) => "<fn>".into(),
1712 }
1713 }
1714
1715 /// The wire form, used for command payloads carried by handlers and for the log.
1716 pub fn to_json(&self) -> serde_json::Value {
1717 use serde_json::{Map as JMap, Value as J};
1718 match self {
1719 Value::Unit => J::Null,
1720 Value::Bool(b) => J::Bool(*b),
1721 Value::Int(i) => J::Number((*i).into()),
1722 Value::Float(_) => serde_json::Number::from_f64(self.as_f64().unwrap_or(0.0))
1723 .map(J::Number)
1724 .unwrap_or(J::Null),
1725 Value::Str(s) => J::String(s.to_string()),
1726 Value::List(xs) => {
1727 let mut out = Vec::with_capacity(xs.len());
1728 xs.for_each(|x| out.push(x.to_json()));
1729 J::Array(out)
1730 }
1731 Value::Map(m) => {
1732 let mut obj = JMap::new();
1733 for (k, v) in m.iter() {
1734 obj.insert(k.display(), v.to_json());
1735 }
1736 J::Object(obj)
1737 }
1738 Value::Data(d) => {
1739 if d.variant.is_none() && d.fields.len() == 1 {
1740 if let Some(v) = d.fields.values().next() {
1741 return v.to_json();
1742 }
1743 }
1744 let mut obj = JMap::new();
1745 if let Some(v) = &d.variant {
1746 obj.insert("c".into(), J::String(v.to_string()));
1747 }
1748 for (k, v) in d.fields.iter() {
1749 obj.insert(k.to_string(), v.to_json());
1750 }
1751 J::Object(obj)
1752 }
1753 Value::Html(h) => h.to_wire(),
1754 Value::Attr(_) | Value::Closure(_) => J::Null,
1755 }
1756 }
1757}
1758
1759impl fmt::Display for Value {
1760 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1761 f.write_str(&self.display())
1762 }
1763}
1764
1765/// A value that cannot be written to a log.
1766///
1767/// [`Value`] has four consumers with different requirements — the evaluator, the log, the wire, and
1768/// the digest — and three of its variants exist only for the first. Encoding one of those is not a
1769/// value to be lowered; it is a program that should not have compiled.
1770#[derive(Clone, Debug, PartialEq, Eq)]
1771pub struct NotStorable {
1772 /// Which variant, by name.
1773 pub kind: &'static str,
1774}
1775
1776impl fmt::Display for NotStorable {
1777 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1778 write!(
1779 f,
1780 "a {} cannot be written to the log: it is {}, not data",
1781 self.kind,
1782 match self.kind {
1783 "closure" => "code",
1784 "attribute" => "part of a view",
1785 _ => "a view",
1786 }
1787 )
1788 }
1789}
1790
1791impl std::error::Error for NotStorable {}
1792
1793/// A lossless encoding of a [`Value`], for the log and for snapshots.
1794///
1795/// [`Value::to_json`] is the *wire* form: it drops the type name of a record and unwraps a
1796/// newtype, because that is what a browser wants. The log needs the opposite — a record that can
1797/// be read back as exactly the value that was written, because replay compares digests. Hence two
1798/// encodings, and a test that says why.
1799///
1800/// # Why this returns a `Result`
1801///
1802/// It used to encode `Html`, `Attr` and `Closure` as `unit` on the grounds that "neither can appear
1803/// in a log". That grounds was an assumption, not a check: nothing stops a program declaring
1804/// `model State: cached: Html`, and the encoding would then write `unit` into the *durable* path,
1805/// silently, and replay would rebuild a different state. A system whose correctness argument is
1806/// "replay is exact" cannot have a lossy branch in the function that makes the log.
1807///
1808/// Until placement can prove such a type never reaches `durable` (Phase 2's effect rows), refusing
1809/// at the boundary is the honest position: the append fails, the process aborts by the same rule as
1810/// any other failed append (§18.5 item 6), and nothing unreadable is ever committed.
1811pub fn value_to_repr(v: &Value) -> Result<serde_json::Value, NotStorable> {
1812 use serde_json::{json, Map as JMap, Value as J};
1813 Ok(match v {
1814 Value::Unit => json!({"$": "unit"}),
1815 Value::Bool(b) => json!({"$": "bool", "v": b}),
1816 Value::Int(i) => json!({"$": "int", "v": i}),
1817 Value::Float(bits) => json!({"$": "float", "v": bits.to_string()}),
1818 Value::Str(s) => json!({"$": "str", "v": s.as_str()}),
1819 Value::List(xs) => {
1820 let mut items = Vec::with_capacity(xs.len());
1821 xs.try_for_each(|x| value_to_repr(x).map(|v| items.push(v)))?;
1822 json!({"$": "list", "v": items})
1823 }
1824 Value::Map(m) => {
1825 let mut pairs = Vec::with_capacity(m.len());
1826 for (k, val) in m.iter() {
1827 pairs.push(json!([value_to_repr(k)?, value_to_repr(val)?]));
1828 }
1829 json!({"$": "map", "v": pairs})
1830 }
1831 Value::Data(d) => {
1832 let mut f = JMap::new();
1833 for (k, val) in d.fields.iter() {
1834 f.insert(k.to_string(), value_to_repr(val)?);
1835 }
1836 json!({
1837 "$": "data",
1838 "t": d.ty.as_ref(),
1839 "c": d.variant.as_deref(),
1840 "f": J::Object(f)
1841 })
1842 }
1843 Value::Html(_) => return Err(NotStorable { kind: "view" }),
1844 Value::Attr(_) => return Err(NotStorable { kind: "attribute" }),
1845 Value::Closure(_) => return Err(NotStorable { kind: "closure" }),
1846 })
1847}
1848
1849pub fn value_from_repr(j: &serde_json::Value) -> Option<Value> {
1850 let tag = j.get("$")?.as_str()?;
1851 Some(match tag {
1852 "unit" => Value::Unit,
1853 "bool" => Value::Bool(j.get("v")?.as_bool()?),
1854 "int" => Value::Int(j.get("v")?.as_i64()?),
1855 "float" => Value::Float(j.get("v")?.as_str()?.parse().ok()?),
1856 "str" => Value::str_(j.get("v")?.as_str()?),
1857 "list" => Value::list(
1858 j.get("v")?
1859 .as_array()?
1860 .iter()
1861 .map(value_from_repr)
1862 .collect::<Option<Vec<_>>>()?,
1863 ),
1864 "map" => {
1865 let mut m = PMap::new();
1866 for pair in j.get("v")?.as_array()? {
1867 let pair = pair.as_array()?;
1868 m = m.insert(
1869 value_from_repr(pair.first()?)?,
1870 value_from_repr(pair.get(1)?)?,
1871 );
1872 }
1873 Value::Map(m)
1874 }
1875 "data" => {
1876 let mut fields = Fields::new();
1877 for (k, val) in j.get("f")?.as_object()? {
1878 fields.insert(Arc::from(k.as_str()), value_from_repr(val)?);
1879 }
1880 Value::data(
1881 Arc::from(j.get("t")?.as_str()?),
1882 j.get("c").and_then(|c| c.as_str()).map(Arc::from),
1883 fields,
1884 )
1885 }
1886 _ => return None,
1887 })
1888}
1889
1890/// Structural digest of a value — the replay-determinism oracle (§4.8).
1891///
1892/// A property of the *value*, not of whoever produced it: two backends that disagree are
1893/// detected by comparing digests, so the digest cannot live in either of them.
1894pub fn digest(v: &Value) -> [u8; 32] {
1895 let mut hasher = blake3::Hasher::new();
1896 hash_into(v, &mut hasher);
1897 *hasher.finalize().as_bytes()
1898}
1899
1900fn hash_into(v: &Value, h: &mut blake3::Hasher) {
1901 match v {
1902 Value::Unit => h.update(&[0]),
1903 Value::Bool(b) => h.update(&[1, *b as u8]),
1904 Value::Int(i) => {
1905 h.update(&[2]);
1906 h.update(&i.to_le_bytes())
1907 }
1908 Value::Float(bits) => {
1909 h.update(&[3]);
1910 h.update(&bits.to_le_bytes())
1911 }
1912 Value::Str(s) => {
1913 h.update(&[4]);
1914 h.update(&(s.len() as u64).to_le_bytes());
1915 h.update(s.as_bytes())
1916 }
1917 Value::List(xs) => {
1918 h.update(&[5]);
1919 h.update(&(xs.len() as u64).to_le_bytes());
1920 xs.for_each(|x| {
1921 hash_into(x, h);
1922 });
1923 h
1924 }
1925 Value::Map(m) => {
1926 h.update(&[6]);
1927 h.update(&(m.len() as u64).to_le_bytes());
1928 for (k, val) in m.iter() {
1929 hash_into(k, h);
1930 hash_into(val, h);
1931 }
1932 h
1933 }
1934 Value::Data(d) => {
1935 h.update(&[7]);
1936 h.update(d.ty.as_bytes());
1937 h.update(d.variant.as_deref().unwrap_or("").as_bytes());
1938 h.update(&(d.fields.len() as u64).to_le_bytes());
1939 for (k, val) in d.fields.iter() {
1940 h.update(k.as_bytes());
1941 hash_into(val, h);
1942 }
1943 h
1944 }
1945 Value::Html(html) => {
1946 h.update(&[8]);
1947 h.update(html.render().as_bytes())
1948 }
1949 Value::Attr(_) => h.update(&[9]),
1950 Value::Closure(_) => h.update(&[10]),
1951 };
1952}
1953
1954/// Every variable an expression reads and does not itself bind.
1955///
1956/// One implementation, because two passes need the same answer for different reasons:
1957/// [`crate::plan`] asks it of a signal function to decide what a dataflow operator has to be
1958/// handed, and a native backend asks it of a `lam` to decide what a closure has to *carry* — a
1959/// second walk that disagreed about one construct would give a compiled closure a field the
1960/// evaluator's environment does not have.
1961pub fn free_vars(c: &Core, bound: &mut BTreeSet<VarId>, out: &mut BTreeSet<VarId>) {
1962 match &c.kind {
1963 CoreKind::Var(v) => {
1964 if !bound.contains(v) {
1965 out.insert(*v);
1966 }
1967 }
1968 CoreKind::Const(_) | CoreKind::Global(_) => {}
1969 CoreKind::Lam { params, body } => {
1970 let added: Vec<VarId> = params
1971 .iter()
1972 .copied()
1973 .filter(|p| bound.insert(*p))
1974 .collect();
1975 free_vars(body, bound, out);
1976 for p in added {
1977 bound.remove(&p);
1978 }
1979 }
1980 CoreKind::App { func, args } => {
1981 free_vars(func, bound, out);
1982 for a in args {
1983 free_vars(a, bound, out);
1984 }
1985 }
1986 CoreKind::Prim { args, .. } => {
1987 for a in args {
1988 free_vars(a, bound, out);
1989 }
1990 }
1991 CoreKind::Let { var, value, body } => {
1992 free_vars(value, bound, out);
1993 let added = bound.insert(*var);
1994 free_vars(body, bound, out);
1995 if added {
1996 bound.remove(var);
1997 }
1998 }
1999 CoreKind::If { cond, then, alt } => {
2000 free_vars(cond, bound, out);
2001 free_vars(then, bound, out);
2002 free_vars(alt, bound, out);
2003 }
2004 CoreKind::Match { scrutinee, arms } => {
2005 free_vars(scrutinee, bound, out);
2006 for a in arms {
2007 let added: Vec<VarId> = a
2008 .pattern
2009 .binders()
2010 .into_iter()
2011 .filter(|p| bound.insert(*p))
2012 .collect();
2013 for e in a.exprs() {
2014 free_vars(e, bound, out);
2015 }
2016 for p in added {
2017 bound.remove(&p);
2018 }
2019 }
2020 }
2021 CoreKind::Make { fields, .. } => {
2022 for (_, f) in fields {
2023 free_vars(f, bound, out);
2024 }
2025 }
2026 CoreKind::Field { base, .. } => free_vars(base, bound, out),
2027 CoreKind::With { base, fields } => {
2028 free_vars(base, bound, out);
2029 for (_, f) in fields {
2030 free_vars(f, bound, out);
2031 }
2032 }
2033 CoreKind::ListLit(items) => {
2034 for i in items {
2035 free_vars(i, bound, out);
2036 }
2037 }
2038 CoreKind::MapLit(pairs) => {
2039 for (k, v) in pairs {
2040 free_vars(k, bound, out);
2041 free_vars(v, bound, out);
2042 }
2043 }
2044 }
2045}
2046
2047#[cfg(test)]
2048mod tests {
2049 use super::*;
2050
2051 /// The property `Value::Float`'s representation exists for: the order the fold uses and the
2052 /// order arithmetic uses are one order.
2053 ///
2054 /// `f64::to_bits` does not have it — `(-1.0).to_bits()` is larger than `(1.0).to_bits()`
2055 /// because the sign bit is the top one — and that is the defect docs/27 §27.8 records. This is
2056 /// the test that would have caught it, checked across the sign, the zeroes and the infinities
2057 /// rather than on one example.
2058 #[test]
2059 fn reals_compare_as_reals_and_round_trip_through_their_key() {
2060 let ladder = [
2061 f64::NEG_INFINITY,
2062 -1e308,
2063 -1.5,
2064 -1.0,
2065 -f64::MIN_POSITIVE,
2066 0.0,
2067 f64::MIN_POSITIVE,
2068 1.0,
2069 1.5,
2070 1e308,
2071 f64::INFINITY,
2072 ];
2073 for w in ladder.windows(2) {
2074 let (a, b) = (Value::float(w[0]), Value::float(w[1]));
2075 assert!(a < b, "{} should order below {}", w[0], w[1]);
2076 assert_eq!(a.as_f64(), Some(w[0]), "and survive the round trip");
2077 }
2078
2079 // The two IEEE values that would otherwise break `Eq`, canonicalised.
2080 assert_eq!(
2081 Value::float(-0.0),
2082 Value::float(0.0),
2083 "`-0.0` and `0.0` are one value, because `Ord` cannot have two of them"
2084 );
2085 assert_eq!(
2086 Value::float(f64::NAN),
2087 Value::float(-f64::NAN),
2088 "and every NaN is one NaN — including a negative one — for the same reason"
2089 );
2090 assert!(
2091 Value::float(f64::NAN) > Value::float(f64::INFINITY),
2092 "NaN has to go somewhere, and above every number is somewhere"
2093 );
2094
2095 // The digest is a function of the value, and a different real is a different digest.
2096 assert_eq!(digest(&Value::float(1.5)), digest(&Value::float(1.5)));
2097 assert_ne!(digest(&Value::float(1.5)), digest(&Value::float(-1.5)));
2098 }
2099
2100 #[test]
2101 fn the_log_encoding_round_trips_exactly() {
2102 // The wire encoding deliberately loses information the browser does not need; the log
2103 // encoding cannot, because replay compares digests of what it reads back.
2104 let v = Value::data(
2105 Arc::from("State"),
2106 None,
2107 Fields::from_iter([(
2108 Arc::from("todos"),
2109 Value::Map(PMap::from_iter([(
2110 Value::str_("k"),
2111 Value::data(
2112 Arc::from("Todo"),
2113 None,
2114 Fields::from_iter([
2115 (Arc::from("done"), Value::Bool(true)),
2116 (Arc::from("n"), Value::Int(-3)),
2117 ]),
2118 ),
2119 )])),
2120 )]),
2121 );
2122 assert_eq!(
2123 value_from_repr(&value_to_repr(&v).unwrap()),
2124 Some(v.clone())
2125 );
2126 let evt = Value::data(
2127 Arc::from("Event"),
2128 Some(Arc::from("Toggled")),
2129 Fields::from_iter([(Arc::from("id"), Value::str_("x"))]),
2130 );
2131 assert_eq!(value_from_repr(&value_to_repr(&evt).unwrap()), Some(evt));
2132 }
2133
2134 #[test]
2135 fn a_value_the_log_cannot_hold_is_refused_rather_than_flattened() {
2136 // This used to encode as `unit`, on the assumption that a view never reaches the log.
2137 // Nothing checked the assumption: `model State: cached: Html` compiles today, and the
2138 // durable path would have written `unit` and replayed a different state — silently, in the
2139 // one place this system's correctness argument does not permit silence.
2140 let view = Value::Html(Arc::new(crate::html::Html::text("hello")));
2141 let err = value_to_repr(&view).expect_err("a view is not data");
2142 assert!(
2143 err.to_string().contains("cannot be written to the log"),
2144 "{err}"
2145 );
2146
2147 // …and nesting does not launder it: a record holding one is refused too.
2148 let state = Value::data(
2149 Arc::from("State"),
2150 None,
2151 Fields::from_iter([(Arc::from("cached"), view.clone())]),
2152 );
2153 assert!(
2154 value_to_repr(&state).is_err(),
2155 "a record holding a view is not data"
2156 );
2157 assert!(
2158 value_to_repr(&Value::list(vec![view.clone()])).is_err(),
2159 "a list holding a view is not data"
2160 );
2161 assert!(
2162 value_to_repr(&Value::Map(PMap::new().insert(Value::str_("k"), view))).is_err(),
2163 "a map holding a view is not data"
2164 );
2165 }
2166
2167 #[test]
2168 fn maps_order_by_key_so_rendering_is_deterministic() {
2169 let m = PMap::new()
2170 .insert(Value::str_("b"), Value::Int(2))
2171 .insert(Value::str_("a"), Value::Int(1));
2172 let v = Value::Map(m);
2173 assert_eq!(v.display(), "{a: 1, b: 2}");
2174 }
2175
2176 #[test]
2177 fn a_newtype_renders_as_its_payload() {
2178 let id = Value::data(
2179 Arc::from("Id"),
2180 None,
2181 Fields::from_iter([(Arc::from("value"), Value::str_("u-1"))]),
2182 );
2183 assert_eq!(id.display(), "u-1");
2184 assert_eq!(id.to_json(), serde_json::json!("u-1"));
2185 }
2186
2187 #[test]
2188 fn a_command_serialises_with_its_variant_tag() {
2189 let cmd = Value::data(
2190 Arc::from("Command"),
2191 Some(Arc::from("Toggle")),
2192 Fields::from_iter([(Arc::from("id"), Value::str_("x"))]),
2193 );
2194 assert_eq!(cmd.to_json(), serde_json::json!({"c": "Toggle", "id": "x"}));
2195 }
2196
2197 #[test]
2198 fn environments_shadow_innermost_first() {
2199 let base = Env::new().extend(vec![(1, Value::Int(1))]);
2200 let inner = base.extend(vec![(1, Value::Int(2))]);
2201 assert_eq!(inner.get(1), Some(&Value::Int(2)));
2202 assert_eq!(base.get(1), Some(&Value::Int(1)));
2203 assert_eq!(inner.get(9), None);
2204 }
2205
2206 #[test]
2207 fn only_the_impure_primitives_carry_atoms_of_their_own() {
2208 assert_eq!(Prim::MergeClients.effects(), vec![Effect::Ingress]);
2209 assert_eq!(Prim::Durable.effects(), vec![Effect::Durable]);
2210 assert_eq!(Prim::NewUuid.effects(), vec![Effect::Nondet]);
2211 assert_eq!(Prim::Now.effects(), vec![Effect::Nondet]);
2212 assert_eq!(Prim::SecretEnv.effects(), vec![Effect::Env]);
2213 assert!(Prim::Add.effects().is_empty());
2214 assert!(
2215 Prim::Fold.effects().is_empty(),
2216 "a fold is pure; `durable` is the effect"
2217 );
2218 assert!(
2219 Prim::MapList.effects().is_empty(),
2220 "`map_list` performs nothing of its own — it performs its argument's row, which is a \
2221 variable and lives in the scheme"
2222 );
2223 }
2224}