beck_core/
bundle.rs

1//! The compiled slice of one component, in the form a client can execute — Mode B's payload.
2//!
3//! [`docs/05-tier-lowering.md`](../../../../../docs/05-tier-lowering.md) §5.1: a Mode-B component
4//! is "the component's pure code compiled to WASM … fine-grained signal graph, local speculative
5//! fold + `seq`-based reconciliation". This module is the *payload* half of that: what a browser
6//! has to be given before it can render the page and guess the next state for itself.
7//!
8//! Four roles cross, and each is here for a reason a Mode-A client does not have:
9//!
10//! * **`view`** — because in Mode B the browser renders. This is the only per-component role; the
11//!   other three belong to the application, which is why two components of one program share
12//!   everything but this.
13//! * **`validate`** and **`fold`** — because optimism is not a trick. "The browser applies the
14//!   expected event to its local copy *speculatively* — legitimate because it runs the *same pure
15//!   fold* the server runs" ([`docs/10-decisions.md`](../../../../../docs/10-decisions.md) D5).
16//!   The same `Core`, not a second implementation of it.
17//! * **`init`** — so a client that has never spoken to the server has a state to render, which is
18//!   what an offline cold start is (D7).
19//!
20//! Plus the **definitions those four reach**, transitively: `Core` refers to top-level definitions
21//! by name ([`CoreKind::Global`]), so a role without them is not executable. That closure *is* the
22//! component's slice, and it is why a bundle is smaller than a program.
23//!
24//! # What a bundle is not
25//!
26//! It is not a program. There is no `Placed`, no signal graph, no type table, no test — nothing
27//! the client would need only in order to *check* something, because the client checks nothing.
28//! The program was checked on the way in; the bundle is what is left when the only remaining
29//! question is how to run it.
30//!
31//! # Why a mirror type rather than `#[derive(Serialize)]` on `Core`
32//!
33//! [`crate::repr`]'s argument, one layer up: a concrete type is where the decisions about what
34//! crosses become *visible and reviewable* rather than implied by whatever fields a struct happens
35//! to have. Two decisions are taken here, and neither should be able to change by accident:
36//!
37//! * **Types are dropped.** A `Core` node carries the [`Ty`] the checker inferred, and the
38//!   evaluator never reads it — it dispatches on values. Carrying resolved types would roughly
39//!   double the payload to say something the only consumer cannot use. A *compiling* client
40//!   backend would need them, and that is a format version rather than a field somebody adds
41//!   quietly: [`FORMAT`] is checked on load.
42//! * **Spans are kept.** They are three integers and they are the difference between "the fold
43//!   failed" and "the fold failed at `todo.beck:47`" in a browser console.
44//!
45//! # Why the bundle names the compiler that made it
46//!
47//! [`Prim`] is encoded as its *position* in the primitive table, which is the compact encoding —
48//! and which silently means something different if the table changes. So a bundle carries
49//! [`shape_id`]: a digest of every primitive's name and number. A kernel built from a different
50//! compiler refuses the bundle instead of executing a `str_len` that used to be a `list_len`. It
51//! is the same rule [`crate::repr`]'s `FORMAT` states for the log — "a misread log is worse than
52//! an unreadable one" — applied to code rather than to data.
53
54use std::collections::{BTreeMap, BTreeSet};
55use std::sync::Arc;
56
57use beck_diag::{FileId, Span};
58use serde::{Deserialize, Serialize};
59
60use crate::command;
61use crate::core::{Arm, Const, Core, CoreKind, Pattern, Prim, VarId};
62use crate::split::Placed;
63use crate::ty::{Tier, Ty};
64
65/// The bundle format version, stamped into every bundle and checked on load.
66///
67/// * `1` — postcard over a mirror of `Core`, with types erased.
68/// * `2` — plus `reads_freshness`: whether the view observes §3.7's freshness dimension. postcard
69///   is not self-describing, so a field added is a format changed; the alternative was a kernel
70///   deciding it by looking for a variable in the view's body, which is a guess about the
71///   splitter's output rather than a fact from it.
72pub const FORMAT: u32 = 2;
73
74/// A component's slice: the code, and nothing else.
75#[derive(Clone, Debug)]
76pub struct Bundle {
77    /// The signal this bundle renders — the component's name in the program, and what
78    /// `beck explain render` prints.
79    pub component: Arc<str>,
80    /// The program's content-derived command-channel id (§4.3). A client that reconnects to a
81    /// server running a different program finds out here rather than by sending it a command it
82    /// cannot decode.
83    pub wire_id: String,
84    /// `(state, session, presence, awareness, freshness, interface) -> Html`.
85    pub view: Core,
86    /// `(state, proposal) -> Result[list[Event], Rejection]`.
87    pub validate: Core,
88    /// `(state, Envelope[Event]) -> state`.
89    pub fold: Core,
90    /// The fold's initial accumulator, as an expression rather than a value: evaluating it is the
91    /// kernel's first act, and a `Value` would have needed a second encoding for the same thing.
92    pub init: Core,
93    /// Every definition the four roles reach, transitively.
94    pub defs: BTreeMap<Arc<str>, Core>,
95    /// What the client may send, resolved: enough to turn a `data-b-click` attribute into the
96    /// `Command` value `validate` expects, and to refuse anything else.
97    pub command: command::Schema,
98    /// Whether this component's client may run [`Bundle::validate`] and [`Bundle::fold`] on what it
99    /// holds — see [`crate::render`] for what decides it, and why holding the state is the whole
100    /// question.
101    pub optimistic: bool,
102    /// Whether this component's `view` reads `freshness()` — §3.7's freshness dimension.
103    ///
104    /// The client needs it for one reason and it is not rendering: a page that does *not* observe
105    /// freshness must keep [`docs/94`](../../../../../docs/94-the-client-report.md) §94.12's
106    /// shortcut, where a confirmation that does not move the state costs no render. Comparing the
107    /// freshness unconditionally would make every confirmation move it — from `Pending(1)` to
108    /// `Confirmed` — and hand every program in the tree back the second render that report
109    /// removed.
110    pub reads_freshness: bool,
111    /// `gestures(step, init)`, when the page keeps client-local interface state — D30's
112    /// non-durable fold.
113    ///
114    /// It travels in the bundle because **this is the only side that can run it**: a gesture never
115    /// reaches the server, so a client that could not fold its own would have interface state
116    /// nobody could advance. `None` for a page that keeps none, which is every page compiled
117    /// before D30.
118    pub gestures: Option<Gestures>,
119}
120
121/// What a client needs to fold its own gestures: the step, where to start, and what to decode.
122#[derive(Clone, Debug)]
123pub struct Gestures {
124    /// `(S, G) -> S`.
125    pub step: Core,
126    /// `S` before any gesture — and what the server rendered against (`crate::render`, `B0522`),
127    /// which is what makes hydration free for a page with interface state.
128    pub init: Core,
129    /// The gesture union, resolved to a decoder.
130    ///
131    /// A **second** schema beside [`Bundle::command`] rather than a widening of it, and the
132    /// separation is the point: §3.5's "the client's entire write surface is `send(cmd)` into a
133    /// typed `Command` union" survives D30 only if a gesture cannot be decoded as a command. Two
134    /// decoders over two unions is what says so.
135    pub schema: command::Schema,
136}
137
138/// Why a bundle could not be read.
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum BadBundle {
141    /// Written by a compiler whose format this kernel does not implement.
142    Format {
143        found: u32,
144        expected: u32,
145    },
146    /// Written by a compiler whose primitives are numbered differently.
147    Shape {
148        found: String,
149        expected: String,
150    },
151    Malformed(String),
152}
153
154impl std::fmt::Display for BadBundle {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            BadBundle::Format { found, expected } => write!(
158                f,
159                "this bundle is format {found} and this kernel reads format {expected}"
160            ),
161            BadBundle::Shape { found, expected } => write!(
162                f,
163                "this bundle was compiled by a different compiler \
164                 (primitives {found}, this kernel {expected})"
165            ),
166            BadBundle::Malformed(why) => write!(f, "this bundle is malformed: {why}"),
167        }
168    }
169}
170
171impl std::error::Error for BadBundle {}
172
173/// A digest of the primitive table: every primitive's name and the number this compiler gives it.
174///
175/// Recomputed rather than stored, so it cannot drift from the table it describes.
176pub fn shape_id() -> String {
177    let mut hasher = blake3::Hasher::new();
178    for (_, prim, _) in crate::prelude::prims() {
179        hasher.update(prim.name().as_bytes());
180        hasher.update(b"=");
181        hasher.update((prim as u32).to_le_bytes().as_slice());
182        hasher.update(b";");
183    }
184    hasher.finalize().to_hex()[..16].to_string()
185}
186
187impl Bundle {
188    /// The bundle for a placed program's component.
189    pub fn of(placed: &Placed) -> Bundle {
190        let roles = &placed.roles;
191        let mut defs = BTreeMap::new();
192        let mut seen = BTreeSet::new();
193        for role in [&roles.view, &roles.validate, &roles.fold, &roles.init] {
194            reachable(role, placed, &mut seen, &mut defs);
195        }
196        // The gesture step is a fifth root, and missing it would ship a bundle whose interface
197        // state could not be advanced: the step calls the program's own functions like any other
198        // role, and nothing else in the bundle reaches them.
199        if let Some(g) = &roles.gestures {
200            reachable(&g.step, placed, &mut seen, &mut defs);
201            reachable(&g.init, placed, &mut seen, &mut defs);
202        }
203        Bundle {
204            component: roles.page_name.clone(),
205            wire_id: placed.wire_id.clone(),
206            view: roles.view.clone(),
207            validate: roles.validate.clone(),
208            fold: roles.fold.clone(),
209            init: roles.init.clone(),
210            defs,
211            command: command::Schema::of(placed),
212            // Read rather than taken as an argument: whether a client may guess is a question
213            // about what crosses to it, answered once in `crate::render`.
214            optimistic: placed.render.optimistic,
215            reads_freshness: placed.render.reads_freshness,
216            gestures: roles.gestures.as_ref().map(|g| Gestures {
217                step: g.step.clone(),
218                init: g.init.clone(),
219                schema: command::Schema::of_union(
220                    placed,
221                    g.gesture_ty.con_name().unwrap_or_default(),
222                ),
223            }),
224        }
225    }
226
227    pub fn to_bytes(&self) -> Vec<u8> {
228        // The only failure postcard has for an owned `Vec` sink is allocation, and a `Wire` built
229        // from a bundle in memory cannot exceed it by construction.
230        postcard::to_allocvec(&Wire::of(self)).expect("a bundle is encodable")
231    }
232
233    pub fn from_bytes(bytes: &[u8]) -> Result<Bundle, BadBundle> {
234        let wire: Wire =
235            postcard::from_bytes(bytes).map_err(|e| BadBundle::Malformed(e.to_string()))?;
236        if wire.format != FORMAT {
237            return Err(BadBundle::Format {
238                found: wire.format,
239                expected: FORMAT,
240            });
241        }
242        let expected = shape_id();
243        if wire.shape != expected {
244            return Err(BadBundle::Shape {
245                found: wire.shape,
246                expected,
247            });
248        }
249        Ok(wire.to_bundle())
250    }
251
252    /// How many `Core` nodes this bundle carries — what `beck explain render` reports as its size
253    /// in the compiler's own units, beside the bytes.
254    pub fn nodes(&self) -> usize {
255        let mut n = 0;
256        for code in [&self.view, &self.validate, &self.fold, &self.init]
257            .into_iter()
258            .chain(self.defs.values())
259        {
260            count(code, &mut n);
261        }
262        n
263    }
264}
265
266/// Everything `code` calls, transitively, added to `defs`.
267fn reachable(
268    code: &Core,
269    placed: &Placed,
270    seen: &mut BTreeSet<Arc<str>>,
271    defs: &mut BTreeMap<Arc<str>, Core>,
272) {
273    let mut names = Vec::new();
274    globals(code, &mut names);
275    for name in names {
276        if !seen.insert(name.clone()) {
277            continue;
278        }
279        // A global with no definition is a prelude name the checker resolved to a primitive, or a
280        // trait method the desugaring turned into an ordinary definition. Either way there is
281        // nothing to carry, and the evaluator resolves it the same way on both tiers.
282        let Some(def) = placed.program.defs.get(&name) else {
283            continue;
284        };
285        defs.insert(name, def.body.clone());
286        reachable(&def.body, placed, seen, defs);
287    }
288}
289
290fn globals(code: &Core, out: &mut Vec<Arc<str>>) {
291    if let CoreKind::Global(name) = &code.kind {
292        out.push(name.clone());
293    }
294    walk(code, &mut |c| globals(c, out));
295}
296
297fn count(code: &Core, n: &mut usize) {
298    *n += 1;
299    walk(code, &mut |c| count(c, n));
300}
301
302/// Apply `f` to each immediate sub-expression.
303///
304/// One walk rather than the same `match` written out at each traversal — and the exhaustive match
305/// is what makes a new [`CoreKind`] variant a compile error here rather than a node the bundle
306/// silently drops.
307fn walk(code: &Core, f: &mut dyn FnMut(&Core)) {
308    match &code.kind {
309        CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => {}
310        CoreKind::Lam { body, .. } => f(body),
311        CoreKind::App { func, args } => {
312            f(func);
313            args.iter().for_each(&mut *f);
314        }
315        CoreKind::Prim { args, .. } => args.iter().for_each(&mut *f),
316        CoreKind::Let { value, body, .. } => {
317            f(value);
318            f(body);
319        }
320        CoreKind::If { cond, then, alt } => {
321            f(cond);
322            f(then);
323            f(alt);
324        }
325        CoreKind::Match { scrutinee, arms } => {
326            f(scrutinee);
327            for arm in arms {
328                arm.exprs().for_each(&mut *f);
329            }
330        }
331        CoreKind::Make { fields, .. } => fields.iter().for_each(|(_, c)| f(c)),
332        CoreKind::Field { base, .. } => f(base),
333        CoreKind::With { base, fields } => {
334            f(base);
335            fields.iter().for_each(|(_, c)| f(c));
336        }
337        CoreKind::ListLit(xs) => xs.iter().for_each(&mut *f),
338        CoreKind::MapLit(kvs) => kvs.iter().for_each(|(k, v)| {
339            f(k);
340            f(v);
341        }),
342    }
343}
344
345// ---------------------------------------------------------------- the encoding
346
347#[derive(Serialize, Deserialize)]
348struct Wire {
349    format: u32,
350    shape: String,
351    component: String,
352    wire_id: String,
353    view: WCore,
354    validate: WCore,
355    fold: WCore,
356    init: WCore,
357    defs: Vec<(String, WCore)>,
358    command: command::Schema,
359    optimistic: bool,
360    reads_freshness: bool,
361    gestures: Option<(WCore, WCore, command::Schema)>,
362}
363
364impl Wire {
365    fn of(b: &Bundle) -> Wire {
366        Wire {
367            format: FORMAT,
368            shape: shape_id(),
369            component: b.component.to_string(),
370            wire_id: b.wire_id.clone(),
371            view: WCore::of(&b.view),
372            validate: WCore::of(&b.validate),
373            fold: WCore::of(&b.fold),
374            init: WCore::of(&b.init),
375            defs: b
376                .defs
377                .iter()
378                .map(|(name, code)| (name.to_string(), WCore::of(code)))
379                .collect(),
380            command: b.command.clone(),
381            optimistic: b.optimistic,
382            reads_freshness: b.reads_freshness,
383            gestures: b
384                .gestures
385                .as_ref()
386                .map(|g| (WCore::of(&g.step), WCore::of(&g.init), g.schema.clone())),
387        }
388    }
389
390    fn to_bundle(&self) -> Bundle {
391        Bundle {
392            component: Arc::from(self.component.as_str()),
393            wire_id: self.wire_id.clone(),
394            view: self.view.to_core(),
395            validate: self.validate.to_core(),
396            fold: self.fold.to_core(),
397            init: self.init.to_core(),
398            defs: self
399                .defs
400                .iter()
401                .map(|(name, code)| (Arc::from(name.as_str()), code.to_core()))
402                .collect(),
403            command: self.command.clone(),
404            optimistic: self.optimistic,
405            reads_freshness: self.reads_freshness,
406            gestures: self.gestures.as_ref().map(|(step, init, schema)| Gestures {
407                step: step.to_core(),
408                init: init.to_core(),
409                schema: schema.clone(),
410            }),
411        }
412    }
413}
414
415/// A `Core` node, minus the type.
416#[derive(Serialize, Deserialize)]
417struct WCore {
418    kind: WKind,
419    /// `(file, start, end)`.
420    span: (u32, u32, u32),
421    tier: u8,
422    /// The three annotations later passes set. Each has a safe default — `false`, `UNORDERED`, `0`
423    /// — so carrying them is a performance decision rather than a correctness one; they are here
424    /// because a browser is the tier least able to afford re-deriving them.
425    last_use: bool,
426    order: u32,
427    locals: u32,
428}
429
430#[derive(Serialize, Deserialize)]
431enum WKind {
432    Const(WConst),
433    Var(VarId),
434    Global(String),
435    Lam {
436        params: Vec<VarId>,
437        body: Box<WCore>,
438    },
439    App {
440        func: Box<WCore>,
441        args: Vec<WCore>,
442    },
443    Prim {
444        op: WPrim,
445        args: Vec<WCore>,
446    },
447    Let {
448        var: VarId,
449        value: Box<WCore>,
450        body: Box<WCore>,
451    },
452    If {
453        cond: Box<WCore>,
454        then: Box<WCore>,
455        alt: Box<WCore>,
456    },
457    Match {
458        scrutinee: Box<WCore>,
459        arms: Vec<WArm>,
460    },
461    Make {
462        ty: String,
463        variant: Option<String>,
464        fields: Vec<(String, WCore)>,
465    },
466    Field {
467        base: Box<WCore>,
468        name: String,
469    },
470    With {
471        base: Box<WCore>,
472        fields: Vec<(String, WCore)>,
473    },
474    ListLit(Vec<WCore>),
475    MapLit(Vec<(WCore, WCore)>),
476}
477
478#[derive(Serialize, Deserialize)]
479enum WConst {
480    Unit,
481    Bool(bool),
482    Int(i64),
483    /// The bit pattern, for [`crate::repr`]'s reason: a decimal rendering is not a round trip.
484    Float(u64),
485    Str(String),
486}
487
488#[derive(Serialize, Deserialize)]
489struct WArm {
490    pattern: WPattern,
491    guard: Option<WCore>,
492    body: WCore,
493    span: (u32, u32, u32),
494}
495
496#[derive(Serialize, Deserialize)]
497enum WPattern {
498    Wildcard,
499    Bind(VarId),
500    Const(WConst),
501    Ctor {
502        variant: String,
503        binds: Vec<(String, WPattern)>,
504    },
505    At {
506        var: VarId,
507        inner: Box<WPattern>,
508    },
509    Or(Vec<WPattern>),
510    List {
511        items: Vec<WPattern>,
512        rest: Option<Option<VarId>>,
513    },
514}
515
516fn span_of(s: Span) -> (u32, u32, u32) {
517    (s.file.0, s.start, s.end)
518}
519
520fn to_span(s: (u32, u32, u32)) -> Span {
521    Span {
522        file: FileId(s.0),
523        start: s.1,
524        end: s.2,
525    }
526}
527
528impl WConst {
529    fn of(c: &Const) -> WConst {
530        match c {
531            Const::Unit => WConst::Unit,
532            Const::Bool(b) => WConst::Bool(*b),
533            Const::Int(i) => WConst::Int(*i),
534            Const::Float(f) => WConst::Float(f.to_bits()),
535            Const::Str(s) => WConst::Str(s.to_string()),
536        }
537    }
538
539    fn to_const(&self) -> Const {
540        match self {
541            WConst::Unit => Const::Unit,
542            WConst::Bool(b) => Const::Bool(*b),
543            WConst::Int(i) => Const::Int(*i),
544            WConst::Float(bits) => Const::Float(f64::from_bits(*bits)),
545            WConst::Str(s) => Const::Str(Arc::from(s.as_str())),
546        }
547    }
548}
549
550impl WPattern {
551    fn of(p: &Pattern) -> WPattern {
552        match p {
553            Pattern::Wildcard => WPattern::Wildcard,
554            Pattern::Bind(v) => WPattern::Bind(*v),
555            Pattern::Const(c) => WPattern::Const(WConst::of(c)),
556            Pattern::Ctor { variant, binds } => WPattern::Ctor {
557                variant: variant.to_string(),
558                binds: binds
559                    .iter()
560                    .map(|(f, p)| (f.to_string(), WPattern::of(p)))
561                    .collect(),
562            },
563            Pattern::At { var, inner } => WPattern::At {
564                var: *var,
565                inner: Box::new(WPattern::of(inner)),
566            },
567            Pattern::Or(alts) => WPattern::Or(alts.iter().map(WPattern::of).collect()),
568            Pattern::List { items, rest } => WPattern::List {
569                items: items.iter().map(WPattern::of).collect(),
570                rest: *rest,
571            },
572        }
573    }
574
575    fn to_pattern(&self) -> Pattern {
576        match self {
577            WPattern::Wildcard => Pattern::Wildcard,
578            WPattern::Bind(v) => Pattern::Bind(*v),
579            WPattern::Const(c) => Pattern::Const(c.to_const()),
580            WPattern::Ctor { variant, binds } => Pattern::Ctor {
581                variant: Arc::from(variant.as_str()),
582                binds: binds
583                    .iter()
584                    .map(|(f, p)| (Arc::from(f.as_str()), p.to_pattern()))
585                    .collect(),
586            },
587            WPattern::At { var, inner } => Pattern::At {
588                var: *var,
589                inner: Box::new(inner.to_pattern()),
590            },
591            WPattern::Or(alts) => Pattern::Or(alts.iter().map(WPattern::to_pattern).collect()),
592            WPattern::List { items, rest } => Pattern::List {
593                items: items.iter().map(WPattern::to_pattern).collect(),
594                rest: *rest,
595            },
596        }
597    }
598}
599
600impl WCore {
601    fn of(c: &Core) -> WCore {
602        WCore {
603            kind: WKind::of(&c.kind),
604            span: span_of(c.span),
605            tier: c.tier as u8,
606            last_use: c.last_use,
607            order: c.order,
608            locals: c.locals,
609        }
610    }
611
612    fn to_core(&self) -> Core {
613        // The type the checker inferred is not carried (see the module docs), so every node is
614        // rebuilt with `Unit`: the evaluator dispatches on values, and a placeholder that is
615        // obviously a placeholder is better than one that looks like an inference.
616        let mut core = Core::new(self.kind.to_kind(), Ty::unit(), to_span(self.span));
617        core.tier = tier_of(self.tier);
618        core.last_use = self.last_use;
619        core.order = self.order;
620        core.locals = self.locals;
621        core
622    }
623}
624
625fn tier_of(byte: u8) -> Tier {
626    // Written out rather than transmuted: `Tier` is a `Copy` enum whose numbering is nobody's
627    // contract, and a bundle from a compiler that reordered it should be refused by `shape_id`
628    // rather than land on a different tier here.
629    match byte {
630        b if b == Tier::Client as u8 => Tier::Client,
631        b if b == Tier::Server as u8 => Tier::Server,
632        b if b == Tier::Data as u8 => Tier::Data,
633        _ => Tier::Any,
634    }
635}
636
637impl WKind {
638    fn of(k: &CoreKind) -> WKind {
639        let fields = |fs: &Vec<(Arc<str>, Core)>| {
640            fs.iter()
641                .map(|(n, c)| (n.to_string(), WCore::of(c)))
642                .collect()
643        };
644        match k {
645            CoreKind::Const(c) => WKind::Const(WConst::of(c)),
646            CoreKind::Var(v) => WKind::Var(*v),
647            CoreKind::Global(name) => WKind::Global(name.to_string()),
648            CoreKind::Lam { params, body } => WKind::Lam {
649                params: params.to_vec(),
650                body: Box::new(WCore::of(body)),
651            },
652            CoreKind::App { func, args } => WKind::App {
653                func: Box::new(WCore::of(func)),
654                args: args.iter().map(WCore::of).collect(),
655            },
656            CoreKind::Prim { op, args } => WKind::Prim {
657                op: WPrim(*op),
658                args: args.iter().map(WCore::of).collect(),
659            },
660            CoreKind::Let { var, value, body } => WKind::Let {
661                var: *var,
662                value: Box::new(WCore::of(value)),
663                body: Box::new(WCore::of(body)),
664            },
665            CoreKind::If { cond, then, alt } => WKind::If {
666                cond: Box::new(WCore::of(cond)),
667                then: Box::new(WCore::of(then)),
668                alt: Box::new(WCore::of(alt)),
669            },
670            CoreKind::Match { scrutinee, arms } => WKind::Match {
671                scrutinee: Box::new(WCore::of(scrutinee)),
672                arms: arms
673                    .iter()
674                    .map(|a| WArm {
675                        pattern: WPattern::of(&a.pattern),
676                        guard: a.guard.as_ref().map(WCore::of),
677                        body: WCore::of(&a.body),
678                        span: span_of(a.span),
679                    })
680                    .collect(),
681            },
682            CoreKind::Make {
683                ty,
684                variant,
685                fields: fs,
686            } => WKind::Make {
687                ty: ty.to_string(),
688                variant: variant.as_ref().map(|v| v.to_string()),
689                fields: fields(fs),
690            },
691            CoreKind::Field { base, name } => WKind::Field {
692                base: Box::new(WCore::of(base)),
693                name: name.to_string(),
694            },
695            CoreKind::With { base, fields: fs } => WKind::With {
696                base: Box::new(WCore::of(base)),
697                fields: fields(fs),
698            },
699            CoreKind::ListLit(xs) => WKind::ListLit(xs.iter().map(WCore::of).collect()),
700            CoreKind::MapLit(kvs) => WKind::MapLit(
701                kvs.iter()
702                    .map(|(k, v)| (WCore::of(k), WCore::of(v)))
703                    .collect(),
704            ),
705        }
706    }
707
708    fn to_kind(&self) -> CoreKind {
709        let fields = |fs: &Vec<(String, WCore)>| {
710            fs.iter()
711                .map(|(n, c)| (Arc::from(n.as_str()), c.to_core()))
712                .collect()
713        };
714        match self {
715            WKind::Const(c) => CoreKind::Const(c.to_const()),
716            WKind::Var(v) => CoreKind::Var(*v),
717            WKind::Global(name) => CoreKind::Global(Arc::from(name.as_str())),
718            WKind::Lam { params, body } => CoreKind::Lam {
719                params: params.as_slice().into(),
720                body: Arc::new(body.to_core()),
721            },
722            WKind::App { func, args } => CoreKind::App {
723                func: Box::new(func.to_core()),
724                args: args.iter().map(WCore::to_core).collect(),
725            },
726            WKind::Prim { op, args } => CoreKind::Prim {
727                op: op.0,
728                args: args.iter().map(WCore::to_core).collect(),
729            },
730            WKind::Let { var, value, body } => CoreKind::Let {
731                var: *var,
732                value: Box::new(value.to_core()),
733                body: Box::new(body.to_core()),
734            },
735            WKind::If { cond, then, alt } => CoreKind::If {
736                cond: Box::new(cond.to_core()),
737                then: Box::new(then.to_core()),
738                alt: Box::new(alt.to_core()),
739            },
740            WKind::Match { scrutinee, arms } => CoreKind::Match {
741                scrutinee: Box::new(scrutinee.to_core()),
742                arms: arms
743                    .iter()
744                    .map(|a| Arm {
745                        pattern: a.pattern.to_pattern(),
746                        guard: a.guard.as_ref().map(WCore::to_core),
747                        body: a.body.to_core(),
748                        span: to_span(a.span),
749                    })
750                    .collect(),
751            },
752            WKind::Make {
753                ty,
754                variant,
755                fields: fs,
756            } => CoreKind::Make {
757                ty: Arc::from(ty.as_str()),
758                variant: variant.as_ref().map(|v| Arc::from(v.as_str())),
759                fields: fields(fs),
760            },
761            WKind::Field { base, name } => CoreKind::Field {
762                base: Box::new(base.to_core()),
763                name: Arc::from(name.as_str()),
764            },
765            WKind::With { base, fields: fs } => CoreKind::With {
766                base: Box::new(base.to_core()),
767                fields: fields(fs),
768            },
769            WKind::ListLit(xs) => CoreKind::ListLit(xs.iter().map(WCore::to_core).collect()),
770            WKind::MapLit(kvs) => CoreKind::MapLit(
771                kvs.iter()
772                    .map(|(k, v)| (k.to_core(), v.to_core()))
773                    .collect(),
774            ),
775        }
776    }
777}
778
779/// A primitive, as its number in the table [`shape_id`] pins.
780///
781/// The number is checked while the bundle is being *decoded* rather than after: a primitive this
782/// compiler does not have is a malformed bundle, and the alternative — substituting some other
783/// primitive and running it — is the failure this whole module exists to prevent.
784#[derive(Clone, Copy)]
785struct WPrim(Prim);
786
787impl Serialize for WPrim {
788    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
789        s.serialize_u32(self.0 as u32)
790    }
791}
792
793impl<'de> Deserialize<'de> for WPrim {
794    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<WPrim, D::Error> {
795        let n = u32::deserialize(d)?;
796        table()
797            .get(n as usize)
798            .copied()
799            .flatten()
800            .map(WPrim)
801            .ok_or_else(|| serde::de::Error::custom(format!("no primitive is numbered {n}")))
802    }
803}
804
805/// The primitive table, by number, built once.
806///
807/// A `Prim` is a fieldless enum with no written discriminants, so the numbers are dense from zero
808/// and a `Vec` indexed by them is the lookup. `None` would mean a primitive the prelude declares no
809/// signature for, which the checker could never have produced.
810fn table() -> &'static [Option<Prim>] {
811    static TABLE: std::sync::OnceLock<Vec<Option<Prim>>> = std::sync::OnceLock::new();
812    TABLE.get_or_init(|| {
813        let prims: Vec<Prim> = crate::prelude::prims()
814            .into_iter()
815            .map(|(_, p, _)| p)
816            .collect();
817        let width = prims.iter().map(|p| *p as usize).max().map_or(0, |m| m + 1);
818        let mut table = vec![None; width];
819        for p in prims {
820            table[p as usize] = Some(p);
821        }
822        table
823    })
824}
825
826#[cfg(test)]
827mod tests {
828    use super::*;
829
830    fn placed(src: &str) -> Placed {
831        let (placed, diags, map) = crate::compile_str("t.beck", src);
832        assert!(!diags.has_errors(), "{}", diags.render(&map));
833        placed.expect("compiles")
834    }
835
836    const TODO: &str = r#"
837model Todo:
838    id: Str
839    text: Str
840    done: Bool
841
842model State:
843    todos: list[Todo]
844
845union Command:
846    Add(id: Str, text: Str)
847    Toggle(id: Str)
848
849union Event:
850    Added(id: Str, text: Str)
851    Toggled(id: Str)
852
853union Rejection:
854    Blank
855
856def apply_event(s: State, env: Envelope[Event]) -> State:
857    match env.body:
858        case Added(id, text):
859            return s.with(todos=list_append(s.todos, Todo(id=id, text=text, done=False)))
860        case Toggled(id):
861            return s
862
863def validate(s: State, p: Proposal) -> Result[list[Event], Rejection]:
864    match p.command:
865        case Add(id, text):
866            if str_is_empty(text):
867                return Err(error=Blank)
868            return Ok(value=[Added(id=id, text=text)])
869        case Toggle(id):
870            return Ok(value=[Toggled(id=id)])
871
872def label(t: Todo) -> Str:
873    return t.text
874
875def render(s: State) -> Html:
876    return ui:
877        ul:
878            for t in s.todos:
879                li: label(t)
880
881@on(server)
882proposals: Stream[Proposal] = merge_clients()
883
884@on(server)
885events: Stream[Event] = decide(proposals, todos, validate)
886
887@on(data)
888todos: Signal[State] = durable(fold(apply_event, State(todos=[]), events))
889
890@on(client)
891page: Signal[Html] = signal_map(todos, render)
892"#;
893
894    #[test]
895    fn a_bundle_round_trips_through_its_bytes() {
896        let placed = placed(TODO);
897        let bundle = Bundle::of(&placed);
898        let bytes = bundle.to_bytes();
899        let back = Bundle::from_bytes(&bytes).expect("reads back");
900
901        assert_eq!(back.component, bundle.component);
902        assert_eq!(back.wire_id, bundle.wire_id);
903        assert_eq!(back.optimistic, bundle.optimistic);
904        assert_eq!(back.nodes(), bundle.nodes());
905        assert_eq!(
906            back.defs.keys().collect::<Vec<_>>(),
907            bundle.defs.keys().collect::<Vec<_>>()
908        );
909    }
910
911    #[test]
912    fn a_bundle_carries_what_its_roles_reach_and_not_the_rest() {
913        let placed = placed(TODO);
914        let bundle = Bundle::of(&placed);
915        // `label` is called by `render`, which is the view.
916        assert!(
917            bundle.defs.contains_key("label"),
918            "{:?}",
919            bundle.defs.keys()
920        );
921        // The roles themselves are carried as roles, not as definitions of the same name.
922        assert!(!bundle.defs.contains_key("page"));
923    }
924
925    #[test]
926    fn a_bundle_from_a_differently_numbered_compiler_is_refused() {
927        let placed = placed(TODO);
928        let bytes = Bundle::of(&placed).to_bytes();
929        let mut wire: Wire = postcard::from_bytes(&bytes).expect("decodes");
930        wire.shape = "0000000000000000".to_string();
931        let forged = postcard::to_allocvec(&wire).expect("encodes");
932
933        match Bundle::from_bytes(&forged) {
934            Err(BadBundle::Shape { found, .. }) => assert_eq!(found, "0000000000000000"),
935            other => panic!("expected a shape refusal, got {other:?}"),
936        }
937    }
938
939    #[test]
940    fn a_bundle_from_a_later_format_is_refused() {
941        let placed = placed(TODO);
942        let bytes = Bundle::of(&placed).to_bytes();
943        let mut wire: Wire = postcard::from_bytes(&bytes).expect("decodes");
944        wire.format = FORMAT + 1;
945        let forged = postcard::to_allocvec(&wire).expect("encodes");
946
947        assert!(matches!(
948            Bundle::from_bytes(&forged),
949            Err(BadBundle::Format { .. })
950        ));
951    }
952}