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