beck_host/
program.rs

1//! The bridge between a compiled program and the runtime that drives it.
2//!
3//! This is the "Roc platform" of Beck (`docs/05-tier-lowering.md` §5.2): an effectful Rust host
4//! owning I/O, scheduling and memory, executing the pure program. The program supplies four
5//! closures the splitter sliced out of the signal graph — `validate`, the fold, its initial state,
6//! and the view — and the host supplies everything those closures are not allowed to have.
7//!
8//! Note what is *not* here: no domain types, no todo, no HTML template. That is the whole claim of
9//! Phase 1 over Phase 0 — the same runtime, with the application arriving as compiled `Core`
10//! rather than as hand-written Rust.
11
12use std::collections::BTreeMap;
13use std::sync::Arc;
14
15use anyhow::{anyhow, Context, Result};
16use beck_core::backend::{Backend, Callable};
17use beck_core::core::CoreKind;
18use beck_core::engine::{Engine, Prepared, Retention, SharedDataflow};
19use beck_core::plan::Plan;
20use beck_core::{Core, Html, Placed, Value};
21
22use crate::record::Envelope;
23
24/// The compiled program plus the capabilities the host holds on its behalf.
25///
26/// Note what is absent: any mention of *how* the program executes. The roles are [`Callable`]s a
27/// [`Backend`] prepared, so a native backend is a different argument to [`Runtime::new`] rather
28/// than a change here — and §4.8's differential test between backends is two `Runtime`s over the
29/// same `Placed`.
30pub struct Runtime {
31    placed: Placed,
32    backend: Arc<dyn Backend>,
33    /// Prepared once at startup: the roles the splitter sliced out of the signal graph.
34    validate: Callable,
35    fold_fn: Callable,
36    view_fn: Callable,
37    /// `awareness(f)`'s `f`, prepared, when the page reads a roster with a payload.
38    ///
39    /// The runtime is what applies it, because the subscribers are its fact rather than the
40    /// graph's: it holds every connection's `Session` and turns each into that client's
41    /// contribution. `None` for a page that reads no roster.
42    awareness_fn: Option<Callable>,
43    /// `gestures(step, init)`'s `step`, prepared, when the page keeps client-local interface
44    /// The accumulator a client starts an interface with, and **all of D30 the server holds**.
45    ///
46    /// There is deliberately no gesture *step* here beside it. The step travels in the
47    /// [`Bundle`](beck_core::Bundle) to the client and is run there and nowhere else, because a
48    /// gesture never reaches the server — so a runtime that could fold one would be a method
49    /// nothing may call, and the next reader would reasonably conclude the server has a part in
50    /// this. It does not: it renders the first paint from this value and cannot advance it.
51    ///
52    /// `Unit` for a page that keeps no interface state, which is what the view's sixth parameter
53    /// is then handed: a role the runtime calls has one arity, so the parameter exists whether or
54    /// not the program reads it.
55    gestures_init: Value,
56    /// The same view as a dataflow plan (§5.3), with every operator prepared. Compiled once and
57    /// shared by every subscription: an [`Engine`] per subscriber holds the arrangements, and this
58    /// holds the code.
59    plan: Arc<Prepared>,
60    init: Value,
61    /// The program's `Command` union, resolved to a decoder. Shared with a Mode B client through
62    /// its bundle, so both tiers decode a command the same way.
63    command: beck_core::command::Schema,
64}
65
66impl Runtime {
67    /// Prepare a program for execution by a given backend.
68    ///
69    /// The backend is an argument rather than a default because a default is how `beck-rt` ends up
70    /// naming one implementation again. This crate does not depend on any backend crate, and that
71    /// is the property worth keeping.
72    pub fn new(placed: Placed, backend: Arc<dyn Backend>) -> Result<Runtime> {
73        let role = |code: &Core, what: &str| -> Result<Callable> {
74            backend
75                .function(code)
76                .map_err(|e| anyhow!("preparing {what}: {e}"))
77        };
78        let validate = role(&placed.roles.validate, "`validate`")?;
79        let fold_fn = role(&placed.roles.fold, "the fold")?;
80        let view_fn = role(&placed.roles.view, "the view")?;
81        let awareness_fn = match &placed.roles.awareness {
82            Some(f) => Some(role(f, "the awareness function")?),
83            None => None,
84        };
85        let gestures_init = match &placed.roles.gestures {
86            Some(g) => backend
87                .constant(&g.init)
88                .map_err(|e| anyhow!("evaluating the initial interface state: {e}"))?,
89            None => Value::Unit,
90        };
91        let plan = Arc::new(
92            Prepared::compile(&placed, backend.as_ref())
93                .map_err(|e| anyhow!("compiling the view plan: {e}"))?,
94        );
95        let init = backend
96            .constant(&placed.roles.init)
97            .map_err(|e| anyhow!("evaluating the initial state: {e}"))?;
98
99        let command = beck_core::command::Schema::of(&placed);
100        Ok(Runtime {
101            placed,
102            backend,
103            validate,
104            fold_fn,
105            view_fn,
106            awareness_fn,
107            gestures_init,
108            plan,
109            init,
110            command,
111        })
112    }
113
114    /// Which backend prepared this program — for a diagnostic, and for the report that says two
115    /// backends disagreed.
116    pub fn backend(&self) -> &'static str {
117        self.backend.name()
118    }
119
120    /// The backend itself, for whoever has to prepare something this runtime did not.
121    ///
122    /// The read model's relational SQL is the caller: a `join`, a `group by` and a `distinct` are
123    /// compiled into a plan at *query* time and a plan is prepared by a backend
124    /// ([`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 9). It
125    /// is the same backend the program runs on, which is the property that matters — a query and
126    /// the page it is a view of are executed by one implementation.
127    pub fn executor(&self) -> &dyn Backend {
128        self.backend.as_ref()
129    }
130
131    pub fn placed(&self) -> &Placed {
132        &self.placed
133    }
134
135    pub fn wire_id(&self) -> &str {
136        &self.placed.wire_id
137    }
138
139    pub fn initial_state(&self) -> Result<Value> {
140        Ok(self.init.clone())
141    }
142
143    /// Build the `Proposal` record the program's `validate` expects.
144    pub fn proposal(&self, actor: &(impl Viewer + ?Sized), command: Value) -> Value {
145        beck_core::edge::proposal(actor.actor(), claims_of(actor), actor.path(), command)
146    }
147
148    /// The authority chokepoint, as the program wrote it: the whole
149    /// `Result[list[Event], Rejection]`.
150    ///
151    /// [`Runtime::validate`] narrows this to "events, or a message", which is what an ingress
152    /// handler needs and what a test cannot use: §21.2's `expect Err(BlankText)` is an assertion
153    /// about the *rejection value*, and rendering it to a string first would make the assertion a
154    /// string comparison.
155    pub fn decide(&self, state: &Value, proposal: &Value) -> Result<Value, String> {
156        (self.validate)(vec![state.clone(), proposal.clone()]).map_err(|e| e.to_string())
157    }
158
159    /// The authority chokepoint. Returns the events a proposal becomes, or why it was refused.
160    pub fn validate(&self, state: &Value, proposal: &Value) -> Result<Vec<Value>, String> {
161        let out =
162            (self.validate)(vec![state.clone(), proposal.clone()]).map_err(|e| e.to_string())?;
163        match out.variant() {
164            Some("Ok") => match out.field("value").and_then(|v| v.as_list()) {
165                Some(events) => Ok(events.to_vec()),
166                None => Err("validate returned Ok without a list of events".into()),
167            },
168            Some("Err") => Err(out
169                .field("error")
170                .map(|e| e.display())
171                .unwrap_or_else(|| "rejected".into())),
172            _ => Err(format!("validate returned {}", out.display())),
173        }
174    }
175
176    /// The replay-pure fold. `env` supplies `seq`, `at` and `actor` **as data** (§3.7).
177    pub fn fold(&self, state: &Value, env: &Envelope, event: Value) -> Result<Value> {
178        (self.fold_fn)(vec![state.clone(), env.to_value(event)])
179            .map_err(|e| anyhow!("folding at seq {}: {e}", env.seq))
180    }
181
182    /// The per-session view. In Mode A this runs server-side and its output is diffed (§5.1).
183    ///
184    /// The roster it renders against is the viewer's own — `edge::presence_of` — because a caller
185    /// with no connection registry is rendering the page one actor sees while looking at it.
186    /// [`Runtime::view_with`] is what an application uses, and it is the same function.
187    pub fn view(&self, state: &Value, actor: &(impl Viewer + ?Sized)) -> Result<Html> {
188        let mine = self.contribution(actor)?;
189        self.view_with_all(
190            state,
191            actor,
192            &beck_core::edge::presence_of(actor.actor()),
193            &mine,
194        )
195    }
196
197    /// This client's own awareness contribution, or an empty roster when the page reads none.
198    ///
199    /// The one-connection case, and it is [`Runtime::view`]'s reason: a caller with no registry is
200    /// rendering the page one actor sees while looking at it, so the roster contains them and
201    /// nobody else.
202    pub fn contribution(&self, actor: &(impl Viewer + ?Sized)) -> Result<Value> {
203        match self.contribution_of(actor)? {
204            None => Ok(beck_core::edge::no_awareness()),
205            Some(mine) => Ok(beck_core::edge::awareness_of(actor.actor(), mine)),
206        }
207    }
208
209    /// What this client contributes to everybody else's roster, or `None` when the page reads no
210    /// awareness.
211    ///
212    /// The bare value rather than a roster of one, because the caller that matters is a *registry*
213    /// holding one of these per connection (`beck_rt::awareness`), and it keys them itself. `None`
214    /// rather than `Unit` so that a program which reads no awareness costs a registry nothing —
215    /// there is a difference between contributing nothing and having nothing to contribute.
216    pub fn contribution_of(&self, actor: &(impl Viewer + ?Sized)) -> Result<Option<Value>> {
217        let Some(f) = &self.awareness_fn else {
218            return Ok(None);
219        };
220        let mine = f(vec![session(actor.actor(), claims_of(actor), actor.path())])
221            .map_err(|e| anyhow!("{e}"))
222            .context("computing this client's awareness")?;
223        Ok(Some(mine))
224    }
225
226    /// The same view, against a roster somebody else is keeping (`crate::presence`).
227    pub fn view_with(
228        &self,
229        state: &Value,
230        actor: &(impl Viewer + ?Sized),
231        here: &Value,
232    ) -> Result<Html> {
233        let mine = self.contribution(actor)?;
234        self.view_with_all(state, actor, here, &mine)
235    }
236
237    /// The view, against both rosters a caller may be keeping.
238    pub fn view_with_all(
239        &self,
240        state: &Value,
241        actor: &(impl Viewer + ?Sized),
242        here: &Value,
243        aware: &Value,
244    ) -> Result<Html> {
245        let out = (self.view_fn)(vec![
246            state.clone(),
247            session(actor.actor(), claims_of(actor), actor.path()),
248            here.clone(),
249            aware.clone(),
250            // Confirmed, and not a parameter, because this is the server's render: what it holds
251            // is the fold over the log, and a guess is something only a Mode B client has. The
252            // checker makes the constant unobservable — a page that reads `freshness()` cannot
253            // render on the server at all (`B0518`) — so this is the value the SSR of a Mode B
254            // page is rendered with and nothing else ever sees it.
255            beck_core::edge::confirmed(),
256            // The client-local accumulator, and `init` for the reason `freshness` above is
257            // `Confirmed`: a server has received no gestures, so what it renders is the interface
258            // before any of them. D30's construct is refused a page that renders on the server
259            // so the only page this reaches is the SSR of a Mode B one — where it is
260            // not an approximation but the correct first paint (`B0522`).
261            self.gestures_init.clone(),
262        ])
263        .map_err(|e| anyhow!("{e}"))
264        .context("rendering the view")?;
265        match out {
266            Value::Html(h) => Ok((*h).clone()),
267            other => Err(anyhow!(
268                "the view produced {} rather than Html",
269                other.display()
270            )),
271        }
272    }
273
274    /// The accumulator a client starts an interface with — D30's `gestures(step, init)`'s `init`.
275    ///
276    /// `Unit` when the program keeps no interface state.
277    pub fn gestures_init(&self) -> &Value {
278        &self.gestures_init
279    }
280
281    /// The view as a dataflow plan — what `beck explain incremental` reports on.
282    pub fn plan(&self) -> &Arc<Plan> {
283        self.plan.plan()
284    }
285
286    /// A maintained view for one subscriber, computing the whole plan itself.
287    ///
288    /// One per subscription, because §3.8's per-session views are "the norm, not the exception" and
289    /// an arrangement below a `per_session` is that subscriber's. Everything *above* it is the same
290    /// computation for everybody — [`Plan::shared`] says which nodes — and this engine holds a copy
291    /// of it. [`Runtime::shared_dataflow`] is the one that does not.
292    pub fn view_engine(&self) -> Result<Engine> {
293        Ok(Engine::new(self.plan.clone()))
294    }
295
296    /// The shared half of the plan — §5.3's "one shared dataflow" — for a process to hold one of.
297    ///
298    /// It is created per application rather than per `Runtime` because what it holds is derived
299    /// from the accumulator, and the accumulator belongs to the application. A `Runtime` with no
300    /// application driving it (`beck test`, the differential harness) never makes one.
301    ///
302    /// `retention` says how long it keeps what a subscriber might still ask for, and comes from the
303    /// application's configuration for the reason `beck_rt::AppConfig::retention` gives.
304    pub fn shared_dataflow(&self, retention: Retention) -> Arc<SharedDataflow> {
305        Arc::new(SharedDataflow::with_retention(self.plan.clone(), retention))
306    }
307
308    /// Render a subscriber's view by maintaining it, rather than by recomputing it.
309    ///
310    /// Identical output to [`Runtime::view`] — `beck-cli/tests/incremental_engine.rs` is the gate,
311    /// over every corpus program and every event of a generated log.
312    pub fn render(
313        &self,
314        engine: &mut Engine,
315        state: &Value,
316        actor: &(impl Viewer + ?Sized),
317        here: &Value,
318        aware: &Value,
319    ) -> Result<Html> {
320        let out = engine
321            .render_all(
322                state,
323                &session(actor.actor(), claims_of(actor), actor.path()),
324                here,
325                aware,
326            )
327            .map_err(|e| anyhow!("{e}"))
328            .context("maintaining the view")?;
329        match out {
330            Value::Html(h) => Ok((*h).clone()),
331            other => Err(anyhow!(
332                "the view produced {} rather than Html",
333                other.display()
334            )),
335        }
336    }
337
338    /// The same maintained render, with the operators that do not read the session taken from a
339    /// dataflow shared with every other subscriber (§5.3).
340    ///
341    /// Returns the version the page reflects, which may be newer than `version`: another subscriber
342    /// may have advanced the shared side first, and a page of the newer state is right where
343    /// unwinding an arrangement back to the older one is not.
344    #[allow(clippy::too_many_arguments)]
345    pub fn render_shared(
346        &self,
347        shared: &SharedDataflow,
348        engine: &mut Engine,
349        state: &Value,
350        version: u64,
351        actor: &(impl Viewer + ?Sized),
352        here: &Value,
353        aware: &Value,
354    ) -> Result<(Html, u64)> {
355        let (out, at) = shared
356            .render_all(
357                engine,
358                state,
359                version,
360                &session(actor.actor(), claims_of(actor), actor.path()),
361                here,
362                aware,
363            )
364            .map_err(|e| anyhow!("{e}"))
365            .context("maintaining the view")?;
366        match out {
367            Value::Html(h) => Ok(((*h).clone(), at)),
368            other => Err(anyhow!(
369                "the view produced {} rather than Html",
370                other.display()
371            )),
372        }
373    }
374
375    /// Prepare an arbitrary `Core` lambda for calling, through the same backend the roles use.
376    ///
377    /// The one caller is the test runner (§21.2), which has to evaluate an `expect` expression with
378    /// `state`, `events` and `result` bound. It goes through [`Backend::function`] rather than
379    /// reaching into an evaluator, so a compiling backend serves it unchanged.
380    pub fn prepare(&self, code: &Core) -> Result<Callable> {
381        self.backend
382            .function(code)
383            .map_err(|e| anyhow!("preparing an expression: {e}"))
384    }
385
386    /// The `Session` value a subscriber's view is rendered against.
387    ///
388    /// Public because the incremental view engine takes it as an input rather than receiving it
389    /// through [`Runtime::view`]: a plan's session is a *node*, and everything not downstream of it
390    /// is what §5.3 shares between subscribers.
391    pub fn session(&self, actor: &(impl Viewer + ?Sized)) -> Value {
392        session(actor.actor(), claims_of(actor), actor.path())
393    }
394
395    /// Decode a command from the wire, against the program's own `Command` union.
396    ///
397    /// The union is resolved to a [`beck_core::command::Schema`] once, at compile time, and both
398    /// tiers decode with it: Mode B's client holds a bundle rather than a program, and a second
399    /// decoder written against a second reading of the same union is the failure mode that is
400    /// worth designing out ([`beck_core::command`]).
401    pub fn decode_command(&self, json: &serde_json::Value) -> Result<Value> {
402        self.command.decode(json).map_err(|e| anyhow!(e))
403    }
404}
405
406use beck_core::edge::session;
407
408/// Who a view is rendered for, or a command proposed by.
409///
410/// A trait rather than a type because the two sources of one are genuinely different and both are
411/// legitimate. A **connection** supplies a `beck_rt::identity::Actor`, which only that module's
412/// `Identity::verify` can make and which carries the claims the provider verified — the impl for it
413/// is there rather than here, because the credential is the host's to check. A **name** supplies
414/// itself: `beck test`'s `when session("ana") sends …`, the differential harness, a benchmark, a
415/// client of the playground's tab server — none of them is a connection, so none of them has a
416/// credential to check, and each of them has no claims because there was nobody to make any.
417///
418/// One code path either way, which is the point: a `&str` and an `Actor` reach the same `Session`
419/// constructor, so a claim cannot appear in one render path and not another.
420pub trait Viewer {
421    fn actor(&self) -> &str;
422
423    /// Empty unless a provider verified them.
424    fn claims(&self) -> &BTreeMap<Arc<str>, Arc<str>> {
425        static NONE: std::sync::OnceLock<BTreeMap<Arc<str>, Arc<str>>> = std::sync::OnceLock::new();
426        NONE.get_or_init(BTreeMap::new)
427    }
428
429    /// Where this viewer is — the route, as the browser last stated it.
430    ///
431    /// Defaulted rather than required, and the default is the application's root: a viewer that is
432    /// not a browser has no route, and `beck test`, the differential harness and every benchmark
433    /// are exactly that. The one implementation that overrides it is the subscription's, because a
434    /// socket is the only thing that can be told the route changed.
435    fn path(&self) -> &str {
436        beck_core::edge::ROOT
437    }
438}
439
440impl Viewer for str {
441    fn actor(&self) -> &str {
442        self
443    }
444}
445
446/// So a caller holding a `&&str` — which is what iterating a `[&str]` gives — needs no ceremony.
447impl<T: Viewer + ?Sized> Viewer for &T {
448    fn actor(&self) -> &str {
449        (**self).actor()
450    }
451
452    fn claims(&self) -> &BTreeMap<Arc<str>, Arc<str>> {
453        (**self).claims()
454    }
455
456    fn path(&self) -> &str {
457        (**self).path()
458    }
459}
460
461impl Viewer for String {
462    fn actor(&self) -> &str {
463        self
464    }
465}
466
467impl Viewer for Arc<str> {
468    fn actor(&self) -> &str {
469        self
470    }
471}
472
473/// A viewer, somewhere.
474///
475/// Who is asking and where they are are separate facts, and this is the pair. It is generic over
476/// the identity half because both sources of one need a route: the document handler wraps the
477/// actor a provider verified with the path of the request that rendered it, and `beck test` wraps
478/// the name a test wrote with the route it wrote beside it. The socket's equivalent is
479/// `session::Subscriber`, which is this pair with a route that can move — an HTTP request is one
480/// route by construction and a subscription is not.
481pub struct At<W> {
482    pub who: W,
483    pub path: Arc<str>,
484}
485
486impl<W: Viewer> Viewer for At<W> {
487    fn actor(&self) -> &str {
488        self.who.actor()
489    }
490
491    fn claims(&self) -> &BTreeMap<Arc<str>, Arc<str>> {
492        self.who.claims()
493    }
494
495    fn path(&self) -> &str {
496        &self.path
497    }
498}
499
500/// A viewer's claims, in the shape [`beck_core::edge::session`] takes them.
501fn claims_of(viewer: &(impl Viewer + ?Sized)) -> impl Iterator<Item = (&str, &str)> {
502    viewer
503        .claims()
504        .iter()
505        .map(|(k, v)| (k.as_ref(), v.as_ref()))
506}
507
508/// A `Core` value's shape, for `beck explain`.
509pub fn describe(c: &Core) -> String {
510    match &c.kind {
511        CoreKind::Lam { params, .. } => format!("fn/{}", params.len()),
512        CoreKind::Global(n) => n.to_string(),
513        CoreKind::Prim { op, .. } => op.name().to_string(),
514        _ => format!("{}", c.ty),
515    }
516}