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}