beck_core/plan.rs
1//! The view, as a dataflow plan rather than as one expression.
2//!
3//! [`docs/05-tier-lowering.md`](../../../../../docs/05-tier-lowering.md) §5.3:
4//!
5//! > a thousand connected users of `todos.map(filter_by(session.user))` must compile to *one*
6//! > shared dataflow whose final per-session operators (filter, project, diff) run per subscriber
7//!
8//! and [`docs/03-type-and-effect-system.md`](../../../../../docs/03-type-and-effect-system.md) §3.8:
9//!
10//! > `remaining` updates by ±1 per event, never by recount.
11//!
12//! [`crate::split`] produces a `Core` *function* of the accumulator — full recompute per event,
13//! which Phase 1 called "semantically final, later made incremental". [`crate::incremental`]
14//! answers *which* vertices a plan could maintain. This module is the plan itself: the same view,
15//! decomposed into operators that a delta can flow through, with [`crate::engine`] as the thing
16//! that flows them.
17//!
18//! # What an operator is
19//!
20//! Two kinds, and the distinction is the whole design:
21//!
22//! * A **delta operator** ([`Op::MapValues`], [`Op::MapList`], [`Op::FilterList`], [`Op::SortBy`],
23//! [`Op::Concat`], [`Op::Flatten`], [`Op::FlatMap`], [`Op::Count`], [`Op::IsEmpty`],
24//! [`Op::Join`], [`Op::ArrangeBy`], [`Op::GroupBy`], [`Op::Restrict`]) holds an
25//! ordered *arrangement* — its output as
26//! a keyed collection — and updates it from the changes at its input. Work is proportional to the
27//! change, not to the collection.
28//! * A **pointwise operator** ([`Op::Pointwise`]) holds a value and recomputes it when an input
29//! changed. That is what today's runtime does for the whole view, so a plan of nothing but
30//! pointwise operators is exactly as fast as no plan at all — and no slower, which is what makes
31//! this safe to switch on for every program.
32//!
33//! Everything the decomposition cannot see through becomes one pointwise operator over the plan
34//! nodes it reads: a `match`, an `if`, a call through a value, a primitive with no delta rule. The
35//! fallback is the reason the engine can be correct for programs it cannot accelerate, and
36//! [`Node::because`] records which construct forced it so `beck explain incremental` can say so.
37//!
38//! # Where the keys come from
39//!
40//! An arrangement is a `BTreeMap` from an ordering key to a value, and the key is what makes the
41//! output's *order* a consequence of the plan rather than of a sort at the end. Iteration order
42//! reaches the rendered page and the replay digest ([`crate::pmap`]), so an incremental view that
43//! produced the right entries in a different order would be a correctness bug, not a cosmetic one.
44//!
45//! | operator | key |
46//! |---|---|
47//! | `map_values(m)` | the map's key — so the arrangement is already in the order `map_values` yields |
48//! | `map_list`, `filter_list` | the input's key, unchanged: neither moves an element |
49//! | `sort_by(xs, k)` | `k(x)` followed by the input's key — a stable sort, expressed as an order |
50//! | `concat_lists([a, b])` | the input's position, followed by that input's key |
51//! | `flatten`, `flat_map` | the input's key, followed by the position inside that element's list |
52//! | `join` | the left input's key — a lookup answers one left row, so nothing of the right's is needed to separate two |
53//! | `arrange_by(xs, k)` | `k(x)` followed by the input's key — `sort_by`'s arrangement, probed by prefix instead of iterated |
54//! | `group_by` | the group's key alone — one entry per group, so the collection's order never reaches it |
55//! | `semi_join`, `anti_join` | the input's key, unchanged: the index decides *which* rows survive, never where they sit |
56//!
57//! # What this is not
58//!
59//! It is not a *query* plan. §4.2 keeps the `Query` sub-language symbolic and nothing compiles one;
60//! this compiles the signal graph, which is a different thing that happens to share the word.
61//! `beck explain query` prints *this*, and [`crate::fuse`] rewrites it.
62
63use std::collections::{BTreeMap, BTreeSet};
64use std::sync::Arc;
65
66use crate::check::Def;
67use crate::core::{Core, CoreKind, Prim, VarId};
68use crate::signal::{signal_elem, Graph, Op as SigOp, SigId};
69use crate::split::{Placed, StateRole};
70use crate::ty::{Tier, Ty};
71
72pub type OpId = usize;
73
74/// Whether the decomposition may read a loop as a join.
75///
76/// The off switch [`docs/08`](../../../../../docs/08-roadmap.md) §8.3 item 8 requires of anything
77/// the compiler decides for you — "a default nobody has run is a claim, so the switched-off path
78/// belongs in a gate beside the fast one". Recognising a join
79/// ([`crate::relate`]) changes which operators a program compiles to without the program saying so,
80/// which is exactly the kind of choice that item is about, and
81/// `scaling.rs::maintaining_a_view_whose_loop_looks_something_up_costs_the_same_at_any_size`
82/// measures **both** settings so the gate carries its own evidence that it can fail.
83///
84/// It is a compile-time switch rather than an `AppConfig` field because a plan is compiled once,
85/// before a runtime exists: `beck explain query --no-join` and `beck explain cost --no-join` are
86/// where a developer reaches it.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub enum Relate {
89 /// Read `for x in xs:` whose body looks something up as an equi-join (docs/99 §99.6).
90 #[default]
91 Recognise,
92 /// Leave every loop as the captured `map_list` its source spells, index nothing, and pay the
93 /// nested loop per event.
94 Refuse,
95}
96
97/// A function an operator applies per element, closed over the plan nodes it reads.
98///
99/// The captures are why this is not simply a `Core` lambda: `lambda t: t.owner == session.actor`
100/// reads the session, which is a *node* — so the operator has to be re-run wholesale when a capture
101/// changes, and per element when only the collection changed. Both are expressible only if the
102/// captured nodes are named.
103#[derive(Clone, Debug)]
104pub struct Fun {
105 /// `Lam` over the captured nodes' variables followed by the element.
106 pub code: Core,
107 pub captures: Vec<OpId>,
108}
109
110/// What one operator does.
111#[derive(Clone, Debug)]
112pub enum Op {
113 /// The durable accumulator, supplied by the caller. The plan's one source.
114 State,
115 /// The subscriber's `Session`. Everything not downstream of it is shareable between
116 /// subscribers (§5.3), and everything downstream of it is that subscriber's — including when
117 /// what moved is the *route* rather than the actor, which is the one field of a session that
118 /// changes while a subscription is open ([`crate::render::SessionUse`]).
119 Session,
120 /// Who is connected — `presence()`, supplied by the caller like the other two sources.
121 ///
122 /// Everything downstream of it is **per subscriber** even though the value is the same for
123 /// everybody, and the reason is a clock rather than a privacy rule: the shared dataflow is
124 /// versioned by the log's `seq` ([`crate::engine::SharedDataflow`]), and presence moves when
125 /// the log does not. Sharing it would need a second version, which is
126 /// [`docs/48`](../../../../../docs/48-identity-report.md) §48.13's first unbuilt item.
127 Presence,
128 /// What everybody is doing — `awareness(f)`, supplied by the caller like the other sources.
129 ///
130 /// [`Op::Presence`]'s rules, for [`Op::Presence`]'s reason: a roster with a payload is not a
131 /// function of the accumulator either, and the shared dataflow is versioned by the log's
132 /// `seq`. A separate source rather than a field of the roster because the two move
133 /// independently — a client that moves its cursor changes this and not presence.
134 Awareness,
135 /// A closed expression, evaluated once when the plan is prepared.
136 Const,
137 /// Recomputed when an input changed. Carries a `Lam` over its inputs.
138 Pointwise {
139 code: Core,
140 },
141 /// `map_values(m)` — where every delta in a Beck program is born, because the accumulator is a
142 /// value and a plan consumes changes. [`crate::pmap::PMap::diff`] is the conversion.
143 MapValues,
144 MapList {
145 f: Fun,
146 },
147 FilterList {
148 f: Fun,
149 },
150 SortBy {
151 f: Fun,
152 },
153 /// `concat_lists([a, b, …])` — a union of delta streams, one per named part.
154 Concat,
155 /// `concat_lists(map_list(xs, f))` as one operator — what [`crate::fuse`] makes of the pair,
156 /// and the shape every `for` loop in a `ui:` block has. Applies `f` and takes the resulting
157 /// list apart in one step, so the list of lists in between is never arranged.
158 FlatMap {
159 f: Fun,
160 },
161 /// `concat_lists(xs)` where `xs` is itself a collection of lists: a flatten.
162 ///
163 /// A `for` loop in a `ui:` block lowers to `concat_lists(map_list(todos, …))` and
164 /// [`crate::fuse`] turns that pair into [`Op::FlatMap`], so this is what remains when the
165 /// collection of lists came from somewhere else — a `map_values` whose values are lists, a
166 /// `sort_by`, or a `map_list` the fusion refused.
167 Flatten,
168 /// `list_len` — §3.8's `remaining`. The arrangement's size, so ±1 per delta and never a
169 /// recount; and it does not force its input to be materialised.
170 Count,
171 IsEmpty,
172 /// The join a loop already contained: `for x in xs:` whose body asks `map_get(m, k(x))`.
173 ///
174 /// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.6 — the
175 /// algebra's first **binary** operator, and the reason it is not a syntax: the two programs in
176 /// the tree that relate two collections already say what they mean, and what they were missing
177 /// was an operator to say it *to*. [`crate::relate`] is the recognition.
178 ///
179 /// Two inputs, and they are not symmetric. The **left** is the collection being looped over.
180 /// The **right** is an *index*: an arrangement whose key's first component is the join key,
181 /// which [`Op::MapValues`] over a `Map` already is. One left row matches at most one right row,
182 /// because an arrangement's keys are unique by construction (§99.5 decision 2), so this is an
183 /// outer equi-join on a unique key and every left row appears exactly once in the output —
184 /// with the match, or without one, which is what `map_get`'s `Option` means.
185 ///
186 /// Maintained from **both** sides (§99.5's bilinear rule): a left row that moved is re-looked
187 /// up, and a right row that moved reaches exactly the left rows whose key it answers, through a
188 /// reverse index this operator keeps. Neither costs the collection.
189 Join {
190 /// The join key, as a function of the left element alone. It captures nothing —
191 /// [`crate::relate`] refuses the shape otherwise — which is what makes the operator's own
192 /// work `O(δ)` rather than `O(n)`.
193 key: Fun,
194 /// What one probe of the right side returns, which is decided by which index is on it.
195 matched: Matching,
196 },
197 /// A second index over a collection, keyed by something other than what orders it — §99.5
198 /// decision 4's `arrange_by`, and [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md)
199 /// §99.9 item 3.
200 ///
201 /// It is the right side of a [`Op::Join`] whose left side asked for a *group*: the collection
202 /// the program wrote `filter_list(xs, lambda y: by(y) == …)` over, arranged so that the
203 /// equality is a range rather than a scan.
204 ///
205 /// **Its arrangement is [`Op::SortBy`]'s, and that is worth saying rather than hiding.** Both
206 /// key an element by `f(x)` followed by the input's key, so both are one `BTreeMap` in which
207 /// equal keys keep the order they arrived in. A sort is that arrangement *iterated*; an index
208 /// is that arrangement *probed*. The engine runs one function for the two, and what differs is
209 /// the consumer — which is why they are two operators rather than one with a flag: nothing may
210 /// fuse a probe the way it fuses a sort, and `beck explain query` should not tell a reader
211 /// their program sorts when it does not.
212 ArrangeBy {
213 /// What to key by, as a function of one element. It captures nothing, for
214 /// [`Op::Join`]'s reason.
215 key: Fun,
216 },
217 /// One value per group, maintained — [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md)
218 /// §99.9 item 6's `group by`, and the operator that answers a question about a group
219 /// **without the group existing**.
220 ///
221 /// Its output is an arrangement keyed by the group's key alone, holding the aggregate, so a
222 /// [`Op::Join`] probes it with [`Matching::Unique`] exactly as it probes a `map_values` — the
223 /// answer is `Some(x)` for a group with rows and `None` for one without, which is what
224 /// `list_min` of a list and of an empty list already return.
225 ///
226 /// **It is not an index and it does not arrange the collection.** [`Op::ArrangeBy`] keys every
227 /// *row* so that a range answers with the group; this keeps, per group, only as much as its
228 /// aggregate needs — a multiset of what the rows projected to for an extreme, a running total
229 /// for a sum. A row that arrives moves that and nothing else, and the aggregate moves or it
230 /// does not: an event that does not change the answer emits no change and nothing downstream
231 /// runs. A `sum` is the aggregate that takes no discount there, because every row that joins
232 /// its group changes it.
233 ///
234 /// **Both ends of that multiset are reachable, and that is the finding.** §99.9 item 6 expected
235 /// `min` and `max` to be asymmetric, because a prefix range of *somebody else's* arrangement
236 /// can be entered from its start and not from its end: bounding `(g, y)` above needs a
237 /// successor of an arbitrary [`crate::Value`] and there is none. A tree this operator builds
238 /// itself is keyed by the projection alone and is bounded at both ends by construction, so
239 /// `max` costs what `min` costs. The asymmetry belonged to the design rather than to the
240 /// problem.
241 GroupBy {
242 /// The group's key, as a function of one row. It captures nothing, for [`Op::Join`]'s
243 /// reason.
244 key: Fun,
245 /// What each row contributes to its group — the projection under the aggregate, and the
246 /// identity when the program asked about the rows themselves.
247 of: Fun,
248 /// Which end of the group is wanted.
249 agg: Agg,
250 },
251 /// The left rows an index answers, or the ones it does not — the algebra's **difference**, and
252 /// the intersection that is its complement
253 /// ([`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 7).
254 ///
255 /// The program wrote `filter_list(xs, lambda x: map_contains(m, k(x)))`, or its negation, and
256 /// what that costs today is [`Op::FilterList`]'s rebuild rule: a predicate that reads `m` is a
257 /// different predicate whenever `m` moves, so a payment arriving reconsiders every invoice.
258 /// [`crate::relate::restriction`] is the recognition, and there is no syntax for the operator
259 /// for [`Op::Join`]'s reason.
260 ///
261 /// **It is the one binary operator whose output is one of its inputs**, and that is the whole
262 /// of §99.5 decision 2's "no representational change at all". A [`Op::Join`] emits a *row* —
263 /// the left value and what it matched — so the collection below it holds something the program
264 /// did not write, which is why a `filter_list` cannot become one: its consumers read the
265 /// element. This emits the left element under the left key, so what a consumer reads is what
266 /// the `filter_list` gave it, entry for entry.
267 ///
268 /// Maintained from both sides, as §99.5's bilinear rule requires, and the **right** half is
269 /// the one no single-collection test can see: an entry arriving in the index takes rows *out*
270 /// of a difference and puts them into an intersection, through the same reverse index
271 /// [`Op::Join`] keeps. A left row that moved is one probe.
272 ///
273 /// **It holds no copy of its left input**, which is what lets a row this operator dropped come
274 /// back when the index entry that dropped it leaves. The value is read from the left input
275 /// itself — its arrangement, or the shadow this operator already keeps of a plain list — so
276 /// the state here is a join key per left row and the reverse index, and never a row.
277 Restrict {
278 /// The key to probe the index by, as a function of the left element alone. It captures
279 /// nothing, for [`Op::Join`]'s reason.
280 key: Fun,
281 /// Which answer keeps the row.
282 keep: Presence,
283 },
284 /// The values in a collection, each once — the algebra's **δ**, and the last row of §99.4
285 /// ([`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 7).
286 ///
287 /// `list_unique(xs)`, which is `lib/collections.beck`'s `unique`: the first occurrence of each
288 /// value, in the order the input held them. **The order is the decision and not a detail.**
289 /// The library has a second duplicate-free list — `elements(set_of(xs))`, which is sorted — and
290 /// both are maintainable; taking the answer a program already had rather than inventing a third
291 /// is the test a second spelling of an old operation has to pass, which is `list_sum`'s rule
292 /// applied to an order instead of to a total.
293 ///
294 /// So the output's key is an **input key**: the smallest one holding each value. That makes the
295 /// output a sub-order of the input's, exactly as [`Op::FilterList`]'s is, and it is why nothing
296 /// downstream had to learn anything — a consumer reads the values in first-occurrence order
297 /// because that is what iterating the arrangement gives.
298 ///
299 /// **What moves is the interesting half.** A value arriving *before* its own standing first
300 /// occurrence moves the published entry — the only operator here whose output entry can change
301 /// key without the value changing — and one leaving promotes the next occurrence rather than
302 /// dropping the value. Both are `O(log n)`, because the operator keeps the input keys holding
303 /// each value in an ordered set and reads one end of it.
304 ///
305 /// It carries no per-element function: a projection is a [`Op::MapList`] above it, which is how
306 /// the program wrote it.
307 Distinct,
308}
309
310/// Which side of an index's answer a [`Op::Restrict`] keeps.
311///
312/// Two operators in one, because they are one delta rule read in two directions: the same probe,
313/// the same reverse index, the same cost, and a program that shows both halves of a partition
314/// shares one index between them (§99.5 decision 4).
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub enum Presence {
317 /// `map_contains(m, k(x))` — kept when the index holds the key. The **intersection** by key,
318 /// which is a semi-join.
319 In,
320 /// `not map_contains(m, k(x))` — kept when it does not. The **difference** by key, which is an
321 /// anti-join, and §99.4's one missing row that a program in the tree was already paying for.
322 NotIn,
323}
324
325impl Presence {
326 /// Whether an index holding the key (or not holding it) keeps the row.
327 pub fn keeps(self, present: bool) -> bool {
328 match self {
329 Presence::In => present,
330 Presence::NotIn => !present,
331 }
332 }
333
334 /// The name that reaches a sharing key and `beck explain query`.
335 pub fn name(self) -> &'static str {
336 match self {
337 Presence::In => "semi_join",
338 Presence::NotIn => "anti_join",
339 }
340 }
341}
342
343/// Which aggregate a [`Op::GroupBy`] maintains.
344///
345/// Three rather than four: `count` is the join's own tally ([`Matching::Count`]) because a count
346/// needs nothing of the row at all.
347///
348/// Every one of them is a function of **which numbers** its group's rows project to and of nothing
349/// else — not of the order they arrived in, not of the order the collection holds them in. That is
350/// what makes a maintained answer and a recomputed one the same value rather than nearly the same
351/// one, and it is why `sum` waited for a spelling rather than for an implementation
352/// ([`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 6). It is a
353/// statement about the answer rather than about the state: what an operator has to *keep* in order
354/// to give it differs per aggregate, and for a sum it is not a multiset at all.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub enum Agg {
357 /// The smallest projection in the group, by [`crate::Value`]'s own total order — which is what
358 /// `list_min` compares, so the maintained answer and the recomputed one are the same function
359 /// of the same set.
360 Min,
361 /// The largest, and it costs what [`Agg::Min`] costs.
362 Max,
363 /// The total of the group's projections — `list_sum`, whose answer is `Int` and whose empty
364 /// group is `0` rather than `None`, which is what [`Matching::Total`] exists to say.
365 ///
366 /// **This one keeps no multiset.** A running total moves by `+n` and `-n` as rows arrive and
367 /// leave, so the group's *distinct values* are not a thing it has to know, and the probe is
368 /// `O(1)` rather than the `O(distinct)` a sum derived from [`Agg::Min`]'s tree would cost. The
369 /// accumulator is wider than the answer for `list_sum`'s reason: the sum is exact and the
370 /// failure is a property of the total, not of the way there.
371 Sum,
372}
373
374impl Agg {
375 /// The name that reaches a sharing key and `beck explain query`.
376 pub fn name(self) -> &'static str {
377 match self {
378 Agg::Min => "min",
379 Agg::Max => "max",
380 Agg::Sum => "sum",
381 }
382 }
383}
384
385/// What one probe of a join's right side returns.
386///
387/// The two are not a detail of the index: they are what the expression the join replaced evaluated
388/// to, so a join that returned the wrong one would render a different page.
389#[derive(Clone, Copy, Debug, PartialEq, Eq)]
390pub enum Matching {
391 /// An [`Op::MapValues`] index, whose keys are unique. `Some(row)` or `None` — which is what
392 /// `map_get` returned and what its callers `match` on.
393 Unique,
394 /// An [`Op::ArrangeBy`] index, whose keys are not. The whole group, in the indexed
395 /// collection's own order, as a `list` — which is what the `filter_list` returned.
396 Group,
397 /// The same index, asked **how many** rather than which — `list_len` over the same
398 /// `filter_list`, whose answer is an `Int`.
399 ///
400 /// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 6's
401 /// first aggregate, and the one the language already had a spelling for. The join keeps a count
402 /// per key beside its reverse index and moves it by ±1 as the index moves, so the answer costs
403 /// nothing and **no group is built**. That is the whole difference from [`Matching::Group`],
404 /// which pays the group's size on every event that touches it.
405 Count,
406 /// The same one-entry-per-group index [`Matching::Unique`] probes, read as a **value rather
407 /// than an option**: [`Op::GroupBy`] with [`Agg::Sum`], whose answer is an `Int`.
408 ///
409 /// A key with no entry is a group with no rows, and the sum of no numbers is `0` — where
410 /// `list_min` of the same empty group is `None`. So the two probes differ in exactly one place
411 /// and it is what a missing entry *means*, which is a property of the aggregate rather than of
412 /// the index.
413 ///
414 /// The third answer is the one worth naming: an entry that is not an `Int` — `None`, as
415 /// [`Op::GroupBy`] publishes it — is a group whose total does not fit one, and probing it
416 /// raises what `list_sum` raises. It is published rather
417 /// than raised at maintenance time because **a group nobody asks about must not fail a render**
418 /// — the recompute only ever sums the groups the loop reaches, and a maintained plan that
419 /// failed on the others would disagree with it about whether the program failed at all.
420 Total,
421}
422
423/// Every operator the engine implements, by name.
424///
425/// Published for the same reason [`crate::fuse::RULES`] is: `fusion.rs` holds this set to the
426/// operators the programs in the tree actually compile to, so an operator with a delta rule and no
427/// program is a hole in the differential rather than a line in a match.
428pub const OPERATORS: &[&str] = &[
429 "state",
430 "session",
431 "presence",
432 "awareness",
433 "const",
434 "recompute",
435 "map_values",
436 "map_list",
437 "filter_list",
438 "sort_by",
439 "concat_lists",
440 "flatten",
441 "flat_map",
442 "list_len",
443 "list_is_empty",
444 "join",
445 "arrange_by",
446 "group_by",
447 "semi_join",
448 "anti_join",
449 "distinct",
450];
451
452impl Op {
453 pub fn name(&self) -> &'static str {
454 match self {
455 Op::State => "state",
456 Op::Session => "session",
457 Op::Presence => "presence",
458 Op::Awareness => "awareness",
459 Op::Const => "const",
460 Op::Pointwise { .. } => "recompute",
461 Op::MapValues => "map_values",
462 Op::MapList { .. } => "map_list",
463 Op::FilterList { .. } => "filter_list",
464 Op::SortBy { .. } => "sort_by",
465 Op::Concat => "concat_lists",
466 Op::Flatten => "flatten",
467 Op::FlatMap { .. } => "flat_map",
468 Op::Count => "list_len",
469 Op::IsEmpty => "list_is_empty",
470 Op::Join { .. } => "join",
471 Op::ArrangeBy { .. } => "arrange_by",
472 Op::GroupBy { .. } => "group_by",
473 // Two names for one variant, because which of them a plan holds is the difference
474 // between a page showing what is outstanding and one showing what is settled, and a
475 // reader of `beck explain query` should not have to open the key line to find out.
476 Op::Restrict {
477 keep: Presence::In, ..
478 } => "semi_join",
479 Op::Restrict {
480 keep: Presence::NotIn,
481 ..
482 } => "anti_join",
483 Op::Distinct => "distinct",
484 }
485 }
486
487 /// Whether this operator is maintained by delta rather than recomputed.
488 pub fn maintained(&self) -> bool {
489 matches!(
490 self,
491 Op::MapValues
492 | Op::MapList { .. }
493 | Op::FilterList { .. }
494 | Op::SortBy { .. }
495 | Op::Concat
496 | Op::Flatten
497 | Op::FlatMap { .. }
498 | Op::Count
499 | Op::IsEmpty
500 | Op::Join { .. }
501 | Op::ArrangeBy { .. }
502 | Op::GroupBy { .. }
503 | Op::Restrict { .. }
504 | Op::Distinct
505 )
506 }
507
508 /// Whether this is an input to the dataflow rather than a step in it.
509 pub fn is_source(&self) -> bool {
510 matches!(
511 self,
512 Op::State | Op::Session | Op::Presence | Op::Awareness | Op::Const
513 )
514 }
515
516 /// What orders this operator's arrangement — the table in this module's own documentation, as
517 /// a sentence, so `beck explain query` states the thing that makes the output *order* a
518 /// consequence of the plan rather than of a sort at the end.
519 pub fn key(&self) -> &'static str {
520 match self {
521 Op::State | Op::Session | Op::Presence | Op::Awareness | Op::Const => "a source",
522 Op::Pointwise { .. } | Op::Count | Op::IsEmpty => "a value, not an arrangement",
523 Op::MapValues => "the map's key",
524 Op::MapList { .. } | Op::FilterList { .. } => "the input's key, unchanged",
525 Op::SortBy { .. } => "the sort key, then the input's key — a stable sort as an order",
526 Op::Concat => "which input, then that input's key",
527 Op::Flatten | Op::FlatMap { .. } => {
528 "the input's key, then the position inside its list"
529 }
530 // §99.5 decision 1 asks for the left key followed by the right key. A lookup matches at
531 // most one right row, so the right component is determined by the left one and adding
532 // it would only make an unmatched row's key shorter than a matched one's. The rule and
533 // this are the same rule: iteration is left-order-major, which is what a `for` over the
534 // left side already means.
535 Op::Join { .. } => "the left input's key — left-order-major, as the loop was",
536 // The same two components `sort_by` has, and the second is load-bearing for a
537 // different reason: it is what makes a group come back in the order the collection
538 // held it, which is the order the `filter_list` this replaced returned.
539 Op::ArrangeBy { .. } => {
540 "the key it indexes by, then the input's key — one range per key"
541 }
542 // One component and no second, which is the difference from `arrange_by` above: this
543 // holds one entry per *group* rather than one per row, so a probe is a point lookup
544 // and the collection's own order never reaches the output.
545 Op::GroupBy { .. } => "the group's key — one entry per group, and no row",
546 // The input's key, exactly as `filter_list` above — which is the point of the operator
547 // rather than a coincidence: it keeps and drops the left collection's own elements, so
548 // nothing of the index's order reaches the output.
549 Op::Restrict { .. } => {
550 "the input's key, unchanged — the index decides which, not where"
551 }
552 // A key of the input, as `filter_list` above — but *which* one is the operator's own
553 // answer rather than the program's, and it is what makes the output's order the order
554 // the values were first seen in.
555 Op::Distinct => "the input's key of each value's first occurrence",
556 }
557 }
558
559 /// Whether this operator's output is an arrangement rather than a value.
560 pub fn is_arrangement(&self) -> bool {
561 matches!(
562 self,
563 Op::MapValues
564 | Op::MapList { .. }
565 | Op::FilterList { .. }
566 | Op::SortBy { .. }
567 | Op::Concat
568 | Op::Flatten
569 | Op::FlatMap { .. }
570 | Op::Join { .. }
571 | Op::ArrangeBy { .. }
572 | Op::GroupBy { .. }
573 | Op::Restrict { .. }
574 | Op::Distinct
575 )
576 }
577
578 /// Every per-element function this operator carries, whatever each is applied to.
579 ///
580 /// One accessor rather than the five-way `if let` that was written out at each of the four
581 /// places that remap captures: a new operator with a [`Fun`] missed at one of them would be a
582 /// capture the plan never renumbered, which is a wrong `OpId` rather than a compile error.
583 ///
584 /// It returns a *list* because [`Op::GroupBy`] carries two, and an accessor that returned the
585 /// first would reintroduce exactly the defect the paragraph above describes — silently, for
586 /// the second one only.
587 pub fn funs_mut(&mut self) -> Vec<&mut Fun> {
588 match self {
589 Op::MapList { f }
590 | Op::FilterList { f }
591 | Op::SortBy { f }
592 | Op::FlatMap { f }
593 | Op::Join { key: f, .. }
594 | Op::ArrangeBy { key: f }
595 | Op::Restrict { key: f, .. } => vec![f],
596 Op::GroupBy { key, of, .. } => vec![key, of],
597 _ => Vec::new(),
598 }
599 }
600
601 /// The same functions, borrowed. Order is the order the engine prepares them in.
602 pub fn funs(&self) -> Vec<&Fun> {
603 match self {
604 Op::MapList { f }
605 | Op::FilterList { f }
606 | Op::SortBy { f }
607 | Op::FlatMap { f }
608 | Op::Join { key: f, .. }
609 | Op::ArrangeBy { key: f }
610 | Op::Restrict { key: f, .. } => vec![f],
611 Op::GroupBy { key, of, .. } => vec![key, of],
612 _ => Vec::new(),
613 }
614 }
615}
616
617#[derive(Clone, Debug)]
618pub struct Node {
619 pub op: Op,
620 pub inputs: Vec<OpId>,
621 /// Set when this operator is a fallback: which construct had no delta rule.
622 pub because: Option<String>,
623 /// Set on a loop whose body looks a collection up and which [`crate::relate`] would not read as
624 /// a join: which of its conditions failed.
625 ///
626 /// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.6's rule for
627 /// the shape inference cannot see — "compile it the slow way and *say so*". It is separate from
628 /// [`Node::because`] because this operator is not a fallback: it has a delta rule and applies
629 /// it, and what it is missing is the *index* that would stop it applying it to everything.
630 pub relate: Option<String>,
631 /// True when this node reads the session, directly or through an input. §5.3's boundary: the
632 /// nodes for which this is false are the shared dataflow, the rest run per subscriber.
633 pub per_session: bool,
634 /// How many operators read this one. Two or more is §5.3's shared prefix, at the granularity
635 /// the engine actually shares at.
636 pub consumers: usize,
637}
638
639/// The view as a dataflow.
640///
641/// Nodes are in dependency order — every input's index is less than its consumer's — so the engine
642/// is one forward pass with no scheduling.
643#[derive(Clone, Debug)]
644pub struct Plan {
645 pub nodes: Vec<Node>,
646 /// Constants, in the same index space as `nodes`, for the ones whose op is [`Op::Const`].
647 pub constants: BTreeMap<OpId, Core>,
648 pub root: OpId,
649 pub state: OpId,
650 pub session: OpId,
651 pub presence: OpId,
652 pub awareness: OpId,
653 /// The declared signals that survived as nodes, so a report can use the program's own names.
654 pub signals: Vec<(Arc<str>, OpId)>,
655}
656
657impl Plan {
658 /// How many *operators* are maintained by delta, and how many are recomputed.
659 ///
660 /// Sources and constants are neither: the accumulator, the session and a string literal are
661 /// inputs to the dataflow rather than steps in it, and counting them as "recomputed" would
662 /// make every program look worse than it is by a fixed amount.
663 pub fn counts(&self) -> (usize, usize) {
664 let operators = self.nodes.iter().filter(|n| !n.op.is_source());
665 let maintained = operators.clone().filter(|n| n.op.maintained()).count();
666 (maintained, operators.count() - maintained)
667 }
668
669 /// The operators whose per-element function captured something that moves **on every event**,
670 /// so the whole collection is reapplied whenever anything happens.
671 ///
672 /// This is the defect [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md)
673 /// §99.3 found by sweeping the tree by hand: a loop body that reads the accumulator is a
674 /// *different function* after every event, so §23.13's rebuild rule reapplies it to every
675 /// element — a nested-loop join with no index, invisible until the collection is large. The
676 /// operators of §99.9 remove it where the shape can be recognised, and §99.6's rule for the
677 /// shape that cannot is "compile it the slow way and say so", which is
678 /// [`Node::because`].
679 ///
680 /// Published so the sweep can be a **standing property rather than a thing somebody re-runs**.
681 /// It was re-run by hand three times, and the third time found a site that had arrived one
682 /// change after the second and been missed — the figure in the document was stale because the
683 /// tree had grown under it ([`docs/08`](../../../../../docs/08-roadmap.md) §8.5.6's third decay
684 /// direction). `incremental.rs::no_program_in_the_tree_reapplies_a_collection_per_event` is
685 /// what re-runs it now.
686 ///
687 /// A **per-subscription** capture is not this and is not returned: a function that captured the
688 /// session is reapplied when a subscriber navigates, which is a route change rather than an
689 /// event, and calling the two the same thing is what made the hand sweep hard to read.
690 pub fn reapplied_per_event(&self) -> Vec<OpId> {
691 captured_per_node(self)
692 .into_iter()
693 .enumerate()
694 .filter(|(_, c)| {
695 c.as_ref()
696 .is_some_and(|(_, cadence)| *cadence == Cadence::PerEvent)
697 })
698 .map(|(i, _)| i)
699 .collect()
700 }
701
702 /// The nodes that do not read the session: §5.3's shared dataflow.
703 pub fn shared(&self) -> Vec<OpId> {
704 (0..self.nodes.len())
705 .filter(|&i| !self.nodes[i].per_session)
706 .collect()
707 }
708
709 /// Compile the view of a sliced program, and fuse it.
710 ///
711 /// Everything downstream — the engine, the read models, both reports — reads the *fused* plan,
712 /// so there is one plan a program has rather than two that could disagree.
713 /// [`Plan::unfused`] is what the differential gate compares against.
714 pub fn compile(placed: &Placed) -> Plan {
715 Plan::compile_with(placed, Relate::default())
716 }
717
718 /// The same, with [`Relate`] said out loud.
719 pub fn compile_with(placed: &Placed, relate: Relate) -> Plan {
720 crate::fuse::fuse(Plan::unfused_with(placed, relate)).0
721 }
722
723 /// A plan for one expression over collections the caller supplies — the read model's SQL,
724 /// compiled into the operators a program's view compiles to
725 /// ([`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 9).
726 ///
727 /// `tables` names the fields of the record the engine is handed as its **state**: the `i`th of
728 /// them holds the `i`th table's rows, and the expression reads it as `Var(i)`. That is the whole
729 /// of the arrangement — a query has no session, no presence and no accumulator of its own, so
730 /// the one source a view already has is the one a query uses too, and no operator here is new.
731 ///
732 /// The expression is *built* rather than written, so it carries no [`CoreKind::Global`] and
733 /// there is nothing for a definition table to answer; the decomposition is given an empty table. The
734 /// consequence worth stating is that a query is held to exactly the recognitions a program is:
735 /// [`crate::relate`] reads the loop and emits the [`Op::Join`], the [`Op::ArrangeBy`] and the
736 /// [`Op::GroupBy`], so a `join` in SQL and a `for` loop that looks something up are the same
737 /// operators with the same delta rules and not two implementations that agree.
738 pub fn of_query(tables: &[Arc<str>], body: &Core) -> Plan {
739 Plan::of_query_with(tables, body, Relate::default())
740 }
741
742 /// The same, with [`Relate`] said out loud — which is what lets a gate measure both settings.
743 pub fn of_query_with(tables: &[Arc<str>], body: &Core, relate: Relate) -> Plan {
744 let defs = BTreeMap::new();
745 let mut b = Builder {
746 defs: &defs,
747 relate,
748 nodes: Vec::new(),
749 constants: BTreeMap::new(),
750 cse: BTreeMap::new(),
751 inlining: Vec::new(),
752 states: &[],
753 state: 0,
754 session: 0,
755 presence: 0,
756 awareness: 0,
757 vertices: BTreeMap::new(),
758 };
759 b.state = b.push(Op::State, Vec::new(), None);
760 b.session = b.push(Op::Session, Vec::new(), None);
761 b.presence = b.push(Op::Presence, Vec::new(), None);
762 b.awareness = b.push(Op::Awareness, Vec::new(), None);
763
764 let state = b.state;
765 let mut scope = Scope::new();
766 for (i, name) in tables.iter().enumerate() {
767 let code = lam(
768 vec![0],
769 Core {
770 kind: CoreKind::Field {
771 base: Box::new(var(0, Ty::unit(), beck_diag::Span::NONE)),
772 name: name.clone(),
773 },
774 ty: Ty::unit(),
775 tier: Tier::Any,
776 span: beck_diag::Span::NONE,
777 last_use: false,
778 order: crate::fields::UNORDERED,
779 locals: 0,
780 },
781 );
782 let id = b.shared(
783 format!("field/{name}/{state}"),
784 Op::Pointwise { code },
785 vec![state],
786 None,
787 );
788 scope.insert(i as VarId, id);
789 }
790 let root = b.expr(body, &scope);
791
792 let mut plan = Plan {
793 nodes: b.nodes,
794 constants: b.constants,
795 root,
796 state: b.state,
797 session: b.session,
798 presence: b.presence,
799 awareness: b.awareness,
800 signals: Vec::new(),
801 };
802 plan.finish();
803 plan.prune();
804 crate::fuse::fuse(plan).0
805 }
806
807 /// The plan as the decomposition produced it, before [`crate::fuse`] rewrites it.
808 ///
809 /// Works from the *graph* rather than from [`crate::split::Roles::view`], for the reason
810 /// [`docs/23`](../../../../../docs/23-incremental-views-report.md) built the graph in the first place: the
811 /// sliced expression has already lost which signal each part came from, and a plan whose nodes
812 /// cannot be named is a plan no report can explain.
813 pub fn unfused(placed: &Placed) -> Plan {
814 Plan::unfused_with(placed, Relate::default())
815 }
816
817 /// The same, with [`Relate`] said out loud.
818 pub fn unfused_with(placed: &Placed, relate: Relate) -> Plan {
819 let graph = &placed.graph;
820 let mut b = Builder {
821 defs: &placed.program.defs,
822 relate,
823 nodes: Vec::new(),
824 constants: BTreeMap::new(),
825 cse: BTreeMap::new(),
826 inlining: Vec::new(),
827 states: &placed.roles.states,
828 state: 0,
829 session: 0,
830 presence: 0,
831 awareness: 0,
832 vertices: BTreeMap::new(),
833 };
834 b.state = b.push(Op::State, Vec::new(), None);
835 b.session = b.push(Op::Session, Vec::new(), None);
836 b.presence = b.push(Op::Presence, Vec::new(), None);
837 b.awareness = b.push(Op::Awareness, Vec::new(), None);
838
839 let root = match graph.by_name.get(&placed.roles.page_name).copied() {
840 Some(page) if page < graph.nodes.len() => b.vertex(graph, page),
841 // A **library** has no page, and an empty graph to look it up in. Its plan is its two
842 // sources and a unit — nothing renders it, and the `unwrap_or(0)` this replaced indexed
843 // vertex zero of a graph with no vertices (docs/27 §27.2).
844 _ => {
845 let id = b.push(Op::Const, Vec::new(), None);
846 b.constants.insert(
847 id,
848 Core {
849 kind: CoreKind::Const(crate::core::Const::Unit),
850 ty: Ty::unit(),
851 tier: Tier::Any,
852 span: beck_diag::Span::NONE,
853 last_use: false,
854 order: crate::fields::UNORDERED,
855 locals: 0,
856 },
857 );
858 id
859 }
860 };
861
862 let mut signals: Vec<(Arc<str>, OpId)> = Vec::new();
863 for (&sig, &id) in &b.vertices {
864 if let Some(name) = &graph.node(sig).name {
865 signals.push((name.clone(), id));
866 }
867 }
868 signals.sort();
869
870 let mut plan = Plan {
871 nodes: b.nodes,
872 constants: b.constants,
873 root,
874 state: b.state,
875 session: b.session,
876 presence: b.presence,
877 awareness: b.awareness,
878 signals,
879 };
880 plan.finish();
881 plan.prune();
882 plan
883 }
884
885 /// Propagate `per_session` forward and count consumers.
886 pub(crate) fn finish(&mut self) {
887 for node in &mut self.nodes {
888 node.per_session = false;
889 node.consumers = 0;
890 }
891 for i in 0..self.nodes.len() {
892 let per = matches!(self.nodes[i].op, Op::Session | Op::Presence | Op::Awareness)
893 || self.nodes[i]
894 .inputs
895 .iter()
896 .any(|&j| self.nodes[j].per_session);
897 self.nodes[i].per_session = per;
898 let captured: Vec<OpId> = self.nodes[i]
899 .op
900 .funs()
901 .iter()
902 .flat_map(|f| f.captures.iter().copied())
903 .collect();
904 if captured.iter().any(|&j| self.nodes[j].per_session) {
905 self.nodes[i].per_session = true;
906 }
907 }
908 for i in 0..self.nodes.len() {
909 for j in self.dependencies(i) {
910 self.nodes[j].consumers += 1;
911 }
912 }
913 }
914
915 /// Drop every operator the plan's roots cannot reach, renumber, and recompute what
916 /// [`Plan::finish`] computes. Returns the old-to-new map.
917 ///
918 /// The decomposition builds an operator for every argument of a call it inlines and for every
919 /// `let`'s value, before it knows whether the body reads them — and a bounded call's arguments
920 /// include one dictionary per method of each bound ([`27`](../../../../../docs/27-the-walls-come-down-report.md)),
921 /// of which the body may use none. Deciding that lazily would mean a scope of thunks rather
922 /// than of operators, which changes the order operators are created in and therefore what
923 /// hash-consing shares; pruning afterwards costs one pass and changes nothing else.
924 ///
925 /// The roots are the page, the two sources, and every **named** signal — a name is projected as
926 /// a read-model table ([`docs/23`](../../../../../docs/23-incremental-views-report.md)), so it
927 /// keeps its operator alive whether or not the page reads it.
928 pub(crate) fn prune(&mut self) -> BTreeMap<OpId, OpId> {
929 let mut live = vec![false; self.nodes.len()];
930 let mut stack = vec![
931 self.root,
932 self.state,
933 self.session,
934 self.presence,
935 self.awareness,
936 ];
937 stack.extend(self.signals.iter().map(|(_, id)| *id));
938 while let Some(id) = stack.pop() {
939 if std::mem::replace(&mut live[id], true) {
940 continue;
941 }
942 stack.extend(self.dependencies(id));
943 }
944
945 let mut map = BTreeMap::new();
946 let mut next = 0;
947 for (i, &keep) in live.iter().enumerate() {
948 if keep {
949 map.insert(i, next);
950 next += 1;
951 }
952 }
953 // Dependency order survives renumbering because it is monotone: an input's index was below
954 // its consumer's, and a monotone map keeps it there.
955 let mut nodes = Vec::with_capacity(next);
956 for (i, node) in std::mem::take(&mut self.nodes).into_iter().enumerate() {
957 if !live[i] {
958 continue;
959 }
960 let mut node = node;
961 node.inputs.iter_mut().for_each(|id| *id = map[id]);
962 for f in node.op.funs_mut() {
963 f.captures.iter_mut().for_each(|id| *id = map[id]);
964 }
965 nodes.push(node);
966 }
967 self.nodes = nodes;
968 self.constants = std::mem::take(&mut self.constants)
969 .into_iter()
970 .filter_map(|(id, c)| map.get(&id).map(|&n| (n, c)))
971 .collect();
972 self.signals.iter_mut().for_each(|(_, id)| *id = map[&*id]);
973 self.root = map[&self.root];
974 self.state = map[&self.state];
975 self.session = map[&self.session];
976 self.presence = map[&self.presence];
977 self.awareness = map[&self.awareness];
978 self.finish();
979 map
980 }
981
982 /// Every node an operator reads, including the ones its per-element function captured.
983 pub fn dependencies(&self, i: OpId) -> Vec<OpId> {
984 let mut out = self.nodes[i].inputs.clone();
985 for f in self.nodes[i].op.funs() {
986 out.extend(f.captures.iter().copied());
987 }
988 out
989 }
990
991 /// The names this plan gives one operator, if any — a declared signal is a name a developer
992 /// wrote, and a report that can use it should.
993 pub fn names_of(&self, i: OpId) -> Vec<&str> {
994 self.signals
995 .iter()
996 .filter(|(_, id)| *id == i)
997 .map(|(n, _)| n.as_ref())
998 .collect()
999 }
1000}
1001
1002/// `beck explain query` — the view as a dataflow plan, operator by operator.
1003///
1004/// [`04`](../../../../../docs/04-compiler-architecture.md) §4.7 asks for this command and
1005/// [`20`](../../../../../docs/20-phase-2-report.md) §20.5 says why it could not exist: "the `Query`
1006/// sub-language is deliberately symbolic and there is no plan to explain". There is one now, and
1007/// this prints it — including the two things a developer cannot see any other way: which operator
1008/// **orders** the output, and which side of §5.3's session cut each one is on.
1009pub fn query_report(plan: &Plan) -> String {
1010 query_report_of(plan, "the page")
1011}
1012
1013/// The same report over a plan whose root is not a page.
1014///
1015/// A `select` compiles to a plan too ([`crate::query`]), and its root is a table of rows rather
1016/// than a rendering — which is the one sentence of this report that would otherwise be false.
1017pub fn query_report_of(plan: &Plan, root: &str) -> String {
1018 use std::fmt::Write;
1019 let mut out = String::new();
1020 let _ = writeln!(
1021 out,
1022 "the view as a dataflow plan (§5.3). Operators are in dependency order, so every input\n\
1023 is above its consumer and the engine is one forward pass.\n"
1024 );
1025 for (i, node) in plan.nodes.iter().enumerate() {
1026 let deps = plan.dependencies(i);
1027 let reads = if deps.is_empty() {
1028 String::new()
1029 } else {
1030 format!(
1031 "← {}",
1032 deps.iter()
1033 .map(|d| format!("#{d}"))
1034 .collect::<Vec<_>>()
1035 .join(" ")
1036 )
1037 };
1038 let names = plan.names_of(i);
1039 let _ = writeln!(
1040 out,
1041 " #{:<3} {:<14} {:<12} {:<12} {}",
1042 i,
1043 node.op.name(),
1044 reads,
1045 if node.per_session {
1046 "per session"
1047 } else {
1048 "shared"
1049 },
1050 match (node.consumers, names.is_empty()) {
1051 (_, false) => format!("`{}`", names.join("`, `")),
1052 (0, true) => String::new(),
1053 (1, true) => String::new(),
1054 (n, true) => format!("read by {n}"),
1055 }
1056 );
1057 if node.op.is_arrangement() {
1058 let _ = writeln!(out, " {:<14} ordered by {}", "", node.op.key());
1059 }
1060 if let Some(why) = &node.because {
1061 let _ = writeln!(out, " {:<14} recomputed: {why}", "");
1062 }
1063 }
1064 let (maintained, recomputed) = plan.counts();
1065 let arrangements = plan.nodes.iter().filter(|n| n.op.is_arrangement()).count();
1066 let _ = writeln!(
1067 out,
1068 "\n #{} is the root — {root}.\n\
1069 \x20 {maintained} maintained, {recomputed} recomputed, {arrangements} of them holding an \
1070 arrangement.",
1071 plan.root
1072 );
1073 out
1074}
1075
1076/// What one operator costs per event, in the units [`crate::engine::Work`] counts.
1077///
1078/// `δ` is how many entries moved at its input and `n` how many its input holds. The distinction is
1079/// the whole point of the engine, so a cost that mentions `n` is a cost worth reading.
1080fn op_cost(plan: &Plan, i: OpId) -> String {
1081 let node = &plan.nodes[i];
1082 // A pointwise operator forces every arrangement it reads into a `Value::List`, which copies
1083 // that arrangement's entries: docs/23 §23.8's "the page's children are still assembled in
1084 // full", located at the operator that does it.
1085 let forced: Vec<OpId> = node
1086 .inputs
1087 .iter()
1088 .copied()
1089 .filter(|&j| plan.nodes[j].op.is_arrangement())
1090 .collect();
1091 match &node.op {
1092 Op::State | Op::Session | Op::Presence | Op::Awareness => {
1093 "— a source, read by reference".to_string()
1094 }
1095 Op::Const => "— evaluated once, when the plan is prepared".to_string(),
1096 Op::Pointwise { .. } if forced.is_empty() => {
1097 "1 recompute, and only when an input moved".to_string()
1098 }
1099 Op::Pointwise { .. } => format!(
1100 "1 recompute + n entries copied, forcing {}",
1101 forced
1102 .iter()
1103 .map(|j| format!("#{j}"))
1104 .collect::<Vec<_>>()
1105 .join(" ")
1106 ),
1107 Op::MapValues => "δ touched — O(δ log n), the persistent map's own diff".to_string(),
1108 Op::MapList { .. } => "δ applications, δ touched".to_string(),
1109 Op::FilterList { .. } => "δ applications, at most δ touched".to_string(),
1110 Op::SortBy { .. } => {
1111 "δ applications, at most 2δ touched — a move is a remove and an insert".to_string()
1112 }
1113 Op::Concat => "δ touched".to_string(),
1114 Op::Flatten => "the entries of each changed element's list".to_string(),
1115 Op::FlatMap { .. } => {
1116 "δ applications, then the entries of each changed element's list".to_string()
1117 }
1118 Op::Count | Op::IsEmpty => "O(1) — the arrangement's size, never a recount".to_string(),
1119 // Both halves of §99.5's bilinear rule, in one line, because a reader who only sees the
1120 // first would think an index answers a question and never receives one.
1121 Op::Join {
1122 matched: Matching::Unique,
1123 ..
1124 } => "δ keys applied on the left, and on the right the rows each moved \
1125 index entry answers — neither is n"
1126 .to_string(),
1127 // The honest half of §99.9 item 3, at the operator that pays it: the scan is gone and the
1128 // group is not. A left row whose group moved is rebuilt whole, because the expression this
1129 // replaced evaluated to a `list`.
1130 Op::Join {
1131 matched: Matching::Group,
1132 ..
1133 } => "δ keys applied on the left, and on the right one group rebuilt per key that \
1134 moved — the group, never the collection"
1135 .to_string(),
1136 Op::Join {
1137 matched: Matching::Count,
1138 ..
1139 } => "δ keys applied on the left, and on the right ±1 per moved index entry — the \
1140 group is counted, never built"
1141 .to_string(),
1142 Op::Join {
1143 matched: Matching::Total,
1144 ..
1145 } => "δ keys applied on the left, and on the right one entry per group whose total \
1146 moved — the group is totalled, never built"
1147 .to_string(),
1148 Op::ArrangeBy { .. } => "δ applications, at most 2δ touched — a move is a remove and an \
1149 insert. The probe is the join's cost, not this one's"
1150 .to_string(),
1151 // Two applications rather than one — the group's key and the projection — and then one
1152 // entry per group whose answer *moved*, which is the half worth stating: an event that
1153 // adds a row behind the extreme changes nothing and nothing downstream of it runs. A
1154 // `sum` is the one aggregate whose answer moves whenever its group does, so it is the one
1155 // that never takes that discount.
1156 Op::GroupBy { agg, .. } => format!(
1157 "2δ applications, at most δ touched — one {} per group, and the group is never built",
1158 agg.name()
1159 ),
1160 // The bilinear rule again, and the right half is the one worth printing: an index entry
1161 // that moved reaches exactly the rows waiting on its key, and each of those either enters
1162 // the output or leaves it. Neither side is n.
1163 Op::Restrict { keep, .. } => format!(
1164 "δ keys applied on the left, and on the right the rows each moved index entry {} \
1165 — neither is n",
1166 match keep {
1167 Presence::In => "admits or withdraws",
1168 Presence::NotIn => "withdraws or admits",
1169 }
1170 ),
1171 // No applications at all — the operator has no per-element function — and the touched
1172 // count is what a value's *first occurrence* moving costs: one remove and one insert, on
1173 // the values that moved rather than on the distinct ones.
1174 Op::Distinct => {
1175 "at most 2δ touched, no applications — a value whose first occurrence moved is a \
1176 remove and an insert"
1177 .to_string()
1178 }
1179 }
1180}
1181
1182/// How often an operator's value moves, which is what decides whether capturing it costs anything.
1183///
1184/// A per-element function that captured another operator is a different function when that
1185/// operator moves, so the whole collection is reconsidered. Whether that is a defect or a
1186/// non-event depends entirely on **what it captured**: a constant never moves, a session moves
1187/// when a subscriber navigates, and anything downstream of the state moves on every event the fold
1188/// admits. Printing the three the same way is what
1189/// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.3 found, and it
1190/// left a reader tracing inputs back to `#0` by hand — one of the real cases in the corpus is two
1191/// hops away.
1192#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1193enum Cadence {
1194 /// Constants all the way down: computed once when the plan is prepared.
1195 Never,
1196 /// The session or the roster. It moves while a subscription is open, but not with the log.
1197 PerSubscription,
1198 /// The state. It moves on every event.
1199 PerEvent,
1200}
1201
1202impl Cadence {
1203 /// What a capture of something moving this often costs, as the whole clause.
1204 ///
1205 /// One sentence per cadence rather than a rate and a reason assembled separately, because the
1206 /// three differ in what a reader should *do* about them and not only in a frequency.
1207 fn line(self, captured: &str) -> String {
1208 match self {
1209 Cadence::PerEvent => format!(
1210 "n applications on every event — its function captured {captured}, which is \
1211 downstream of the state"
1212 ),
1213 Cadence::PerSubscription => format!(
1214 "n applications when the session moves, which is not per event — its function \
1215 captured {captured}"
1216 ),
1217 Cadence::Never => {
1218 format!("no cost per event — its function captured {captured}, which never moves")
1219 }
1220 }
1221 }
1222}
1223
1224/// Every operator's [`Cadence`], in one pass.
1225///
1226/// One pass is enough because the plan's nodes are in dependency order — every input's index is
1227/// less than its consumer's, which [`Plan`] states as its invariant — so an input's answer is
1228/// always already in hand.
1229fn cadences(plan: &Plan) -> Vec<Cadence> {
1230 let mut out: Vec<Cadence> = Vec::with_capacity(plan.nodes.len());
1231 for (i, node) in plan.nodes.iter().enumerate() {
1232 let own = match node.op {
1233 Op::State => Cadence::PerEvent,
1234 Op::Session | Op::Presence | Op::Awareness => Cadence::PerSubscription,
1235 _ => Cadence::Never,
1236 };
1237 let inherited = node
1238 .inputs
1239 .iter()
1240 .map(|&j| {
1241 debug_assert!(j < i, "the plan's nodes are in dependency order");
1242 out[j]
1243 })
1244 .max()
1245 .unwrap_or(Cadence::Never);
1246 out.push(own.max(inherited));
1247 }
1248 out
1249}
1250
1251/// What each operator's per-element function captured, and how often that moves.
1252///
1253/// `None` for an operator that has no per-element function, or whose function captured nothing.
1254/// The join's key function is deliberately excluded: it captures nothing by construction, and an
1255/// empty capture line for it would read as a cost.
1256///
1257/// **One computation, several readers**, which is [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md)
1258/// §99.9 item 2's lesson applied a second time: [`cost_report`] prints these, its summary counts
1259/// them, and [`Plan::reapplied_per_event`] answers a question about them. Three readers deriving
1260/// the same fact separately is how the tally and the body came to disagree the first time.
1261fn captured_per_node(plan: &Plan) -> Vec<Option<(Vec<OpId>, Cadence)>> {
1262 let moves = cadences(plan);
1263 plan.nodes
1264 .iter()
1265 .map(|node| {
1266 let captures = match &node.op {
1267 Op::MapList { f } | Op::FilterList { f } | Op::SortBy { f } | Op::FlatMap { f } => {
1268 f.captures.clone()
1269 }
1270 _ => Vec::new(),
1271 };
1272 captures
1273 .iter()
1274 .map(|&j| moves[j])
1275 .max()
1276 .map(|worst| (captures, worst))
1277 })
1278 .collect()
1279}
1280
1281/// One operator's line in the report, and the facts the summary is counted from.
1282///
1283/// The summary is derived from these rather than recomputed beside them, which is the shape of the
1284/// defect this replaced: the tally counted one thing and the body printed another, so a program
1285/// whose loop captured the accumulator was told "1 of 29" when two operators cost `O(n)`.
1286struct Charge {
1287 cost: String,
1288 /// What the operator's per-element function captured, and how often that moves.
1289 captured: Option<(Vec<OpId>, Cadence)>,
1290 /// Why this operator costs `O(n)` per event, or `None` when it does not.
1291 linear: Option<Linear>,
1292}
1293
1294#[derive(Clone, Copy, PartialEq, Eq)]
1295enum Linear {
1296 /// It forces an arrangement into a list.
1297 Forced,
1298 /// Its per-element function captured something that moves with the state.
1299 Captured,
1300}
1301
1302/// `beck explain cost` — what one event costs this view.
1303///
1304/// [`20`](../../../../../docs/20-phase-2-report.md) §20.5 left this command unbuilt with a reason
1305/// rather than a shrug: `beck explain place` already prints every candidate's cost, and "whether a
1306/// separate `cost` view earns its place is a question for when there is a second cost dimension to
1307/// show". The plan is that second dimension. Placement costs are about *where* a definition runs,
1308/// once, at compile time; these are about what the program does *per event*, for as long as it is
1309/// running, and no placement decision can see them.
1310pub fn cost_report(plan: &Plan) -> String {
1311 use std::fmt::Write;
1312 let mut captures = captured_per_node(plan);
1313 let charges: Vec<Charge> = (0..plan.nodes.len())
1314 .map(|i| {
1315 let cost = op_cost(plan, i);
1316 let captured = std::mem::take(&mut captures[i]);
1317 let linear = if cost.contains("n entries copied") {
1318 Some(Linear::Forced)
1319 } else if captured
1320 .as_ref()
1321 .is_some_and(|(_, c)| *c == Cadence::PerEvent)
1322 {
1323 Some(Linear::Captured)
1324 } else {
1325 None
1326 };
1327 Charge {
1328 cost,
1329 captured,
1330 linear,
1331 }
1332 })
1333 .collect();
1334
1335 let mut out = String::new();
1336 let _ = writeln!(
1337 out,
1338 "what one event costs this view, in the units the engine counts (§3.8).\n\
1339 \x20 δ is how many entries moved; n is how many the collection holds.\n"
1340 );
1341 for (i, charge) in charges.iter().enumerate() {
1342 let _ = writeln!(
1343 out,
1344 " #{:<3} {:<14} {}",
1345 i,
1346 plan.nodes[i].op.name(),
1347 charge.cost
1348 );
1349 // An operator whose per-element function reads another operator is a different function
1350 // when that one moves, so the whole collection is reconsidered. Saying *what it captured*
1351 // is not enough — a reader needs to know how often that thing moves, which is the
1352 // difference between a non-event and the most expensive line in the report.
1353 if let Some((captured, cadence)) = &charge.captured {
1354 let names = captured
1355 .iter()
1356 .map(|j| format!("#{j}"))
1357 .collect::<Vec<_>>()
1358 .join(" or ");
1359 let _ = writeln!(out, " {:<14} {}", "", cadence.line(&names));
1360 // Only under a per-event capture, which is the one cadence a join would have removed.
1361 // Under a captured `const` the sentence would be true and pointless.
1362 if *cadence == Cadence::PerEvent {
1363 if let Some(why) = &plan.nodes[i].relate {
1364 let _ = writeln!(
1365 out,
1366 " {:<14} not read as a relational operator (docs/99 §99.6): {why}",
1367 ""
1368 );
1369 }
1370 }
1371 }
1372 }
1373 let _ = writeln!(out);
1374
1375 let of = |which: Linear| -> Vec<String> {
1376 charges
1377 .iter()
1378 .enumerate()
1379 .filter(|(_, c)| c.linear == Some(which))
1380 .map(|(i, _)| format!("#{i}"))
1381 .collect()
1382 };
1383 let (forced, captured) = (of(Linear::Forced), of(Linear::Captured));
1384 let total = forced.len() + captured.len();
1385 if total == 0 {
1386 let _ = writeln!(
1387 out,
1388 " Nothing here is proportional to the collection: no operator forces an arrangement\n\
1389 \x20 into a list, and no per-element function captured anything that moves with the\n\
1390 \x20 state, so one event costs what the event changed."
1391 );
1392 } else {
1393 // Two reasons, counted together and named apart. They are not the same defect and they do
1394 // not have the same fix: one is a constant factor of the arrangement's representation, and
1395 // the other is a program that escaped the view algebra.
1396 let _ = writeln!(
1397 out,
1398 " {total} of {} operators cost O(n) per event, for {} reason{}:",
1399 plan.nodes.len(),
1400 if forced.is_empty() || captured.is_empty() {
1401 "one"
1402 } else {
1403 "two"
1404 },
1405 if forced.is_empty() || captured.is_empty() {
1406 ""
1407 } else {
1408 "s"
1409 }
1410 );
1411 if !forced.is_empty() {
1412 let _ = writeln!(
1413 out,
1414 " {} a recompute needs a `list` and an arrangement is a keyed collection —\n\
1415 \x20 docs/23 §23.8's remaining constant factor.",
1416 forced.join(" ")
1417 );
1418 }
1419 if !captured.is_empty() {
1420 let _ = writeln!(
1421 out,
1422 " {} a per-element function captured the state, so the whole collection is\n\
1423 \x20 reconsidered on every event — docs/99 §99.3. `beck explain query`\n\
1424 \x20 says whether the algebra has an operator this shape was not read as.",
1425 captured.join(" ")
1426 );
1427 }
1428 }
1429 let _ = writeln!(
1430 out,
1431 "\n These are the plan's arithmetic rather than a measurement: `Work` is what\n\
1432 \x20 `Engine::render` counts, so `measure_incremental` checks this arithmetic against the\n\
1433 \x20 count rather than against a clock. What an operator does *inside* a per-element\n\
1434 \x20 function is not in these lines and is in `Work::steps`, which is what the backend\n\
1435 \x20 executed — the number that tells an opaque operator's cost from its arity."
1436 );
1437 out
1438}
1439
1440struct Builder<'a> {
1441 /// The definitions a call may be inlined from. The whole `Program` was never read for
1442 /// anything else, and a query compiled by [`Plan::of_query`] has none — its expression was
1443 /// built rather than written, so it carries no [`CoreKind::Global`] to resolve.
1444 defs: &'a BTreeMap<Arc<str>, Def>,
1445 relate: Relate,
1446 nodes: Vec<Node>,
1447 constants: BTreeMap<OpId, Core>,
1448 /// Structural hash-consing, so `state.todos` read from two places is one operator with two
1449 /// consumers rather than two operators. That is the fact §5.3's arrangement sharing is about,
1450 /// and it has to be a property of the plan before it can be a property of the engine.
1451 cse: BTreeMap<String, OpId>,
1452 /// Definitions currently being inlined, so a recursive one falls back rather than looping.
1453 inlining: Vec<Arc<str>>,
1454 states: &'a [StateRole],
1455 state: OpId,
1456 session: OpId,
1457 presence: OpId,
1458 awareness: OpId,
1459 vertices: BTreeMap<SigId, OpId>,
1460}
1461
1462/// The symbolic environment: a program variable, and the operator that produces its value.
1463type Scope = BTreeMap<VarId, OpId>;
1464
1465impl Builder<'_> {
1466 fn push(&mut self, op: Op, inputs: Vec<OpId>, because: Option<String>) -> OpId {
1467 self.nodes.push(Node {
1468 op,
1469 inputs,
1470 because,
1471 relate: None,
1472 per_session: false,
1473 consumers: 0,
1474 });
1475 self.nodes.len() - 1
1476 }
1477
1478 /// Push, or reuse an identical operator already in the plan.
1479 fn shared(&mut self, key: String, op: Op, inputs: Vec<OpId>, because: Option<String>) -> OpId {
1480 if let Some(&id) = self.cse.get(&key) {
1481 return id;
1482 }
1483 let id = self.push(op, inputs, because);
1484 self.cse.insert(key, id);
1485 id
1486 }
1487
1488 // ---------------------------------------------------------------------------------------
1489 // The graph
1490 // ---------------------------------------------------------------------------------------
1491
1492 /// The operator one signal vertex's value comes from.
1493 fn vertex(&mut self, graph: &Graph, id: SigId) -> OpId {
1494 let id = follow_alias(graph, id);
1495 if let Some(&done) = self.vertices.get(&id) {
1496 return done;
1497 }
1498 let node = graph.node(id);
1499 // A durable accumulator is where the plan's sources are: the state parameter, or the field
1500 // of it that this fold occupies when several were fused.
1501 if let Some(role) = self.states.iter().find(|s| s.node == id) {
1502 let out = match &role.field {
1503 None => self.state,
1504 Some(f) => {
1505 let code = lam(
1506 vec![0],
1507 Core {
1508 kind: CoreKind::Field {
1509 base: Box::new(var(0, Ty::unit(), node.span)),
1510 name: f.clone(),
1511 },
1512 ty: role.ty.clone(),
1513 tier: Tier::Any,
1514 span: node.span,
1515 last_use: false,
1516 order: crate::fields::UNORDERED,
1517 locals: 0,
1518 },
1519 );
1520 let state = self.state;
1521 self.shared(
1522 format!("field/{f}/{state}"),
1523 Op::Pointwise { code },
1524 vec![state],
1525 None,
1526 )
1527 }
1528 };
1529 self.vertices.insert(id, out);
1530 return out;
1531 }
1532
1533 let out = match &node.op {
1534 SigOp::Map { f } => {
1535 let input = self.vertex(graph, node.inputs[0]);
1536 self.apply(
1537 f,
1538 vec![input],
1539 &Scope::new(),
1540 signal_elem(&node.ty),
1541 node.span,
1542 )
1543 }
1544 SigOp::Map2 { f } => {
1545 let a = self.vertex(graph, node.inputs[0]);
1546 let b = self.vertex(graph, node.inputs[1]);
1547 self.apply(
1548 f,
1549 vec![a, b],
1550 &Scope::new(),
1551 signal_elem(&node.ty),
1552 node.span,
1553 )
1554 }
1555 SigOp::Presence => self.presence,
1556 // Like presence, and for the same reason: what `f` is applied to is every *other*
1557 // subscriber's session, which this dataflow does not hold. The runtime does
1558 // (`beck_rt::awareness`), and hands the answer in as a source.
1559 SigOp::Awareness { .. } => self.awareness,
1560 // Not a source: a **constant**, and that is the whole statement this plan makes about
1561 // freshness. A plan is what the *server* renders through, and a server renders the
1562 // state it has recorded — so `freshness()` here is `Confirmed` and never moves. The
1563 // engine therefore treats it as it treats a string literal: evaluated once when the
1564 // plan is prepared, and never a reason to recompute anything below it.
1565 //
1566 // A page that branched on it would be refused Mode A before reaching here
1567 // (`crate::render`, `B0518`); what does reach here is the SSR of a Mode B page, whose
1568 // first paint is by construction the confirmed one.
1569 SigOp::Freshness => {
1570 let id = self.push(Op::Const, Vec::new(), None);
1571 self.constants.insert(
1572 id,
1573 Core {
1574 kind: CoreKind::Make {
1575 ty: Arc::from("Freshness"),
1576 variant: Some(Arc::from("Confirmed")),
1577 fields: Vec::new(),
1578 },
1579 ty: Ty::con("Freshness"),
1580 tier: Tier::Any,
1581 span: node.span,
1582 last_use: false,
1583 order: crate::fields::UNORDERED,
1584 locals: 0,
1585 },
1586 );
1587 id
1588 }
1589 // A constant, for `SigOp::Freshness`'s reason stated about the other client-held fact.
1590 // A plan is what the *server* renders through and a server has received no gestures, so
1591 // the accumulator here is `init` and never moves — which is not an approximation but
1592 // the right answer: before any gesture, the interface state *is* its initial value.
1593 //
1594 // A page that reads one is refused Mode A (`crate::render`, `B0522`), so what reaches
1595 // here is the SSR of a Mode B page, whose first paint is by construction the one with
1596 // no gesture applied. The client's kernel holds the accumulator from then on.
1597 SigOp::Gestures { init, .. } => {
1598 let id = self.push(Op::Const, Vec::new(), None);
1599 self.constants.insert(id, init.clone());
1600 id
1601 }
1602 SigOp::PerSession { f } => {
1603 let input = self.vertex(graph, node.inputs[0]);
1604 let session = self.session;
1605 self.apply(
1606 f,
1607 vec![input, session],
1608 &Scope::new(),
1609 signal_elem(&node.ty),
1610 node.span,
1611 )
1612 }
1613 // The slicer has already refused every other op before a plan is asked for — a stream
1614 // under a view, a fold that is not durable, a cycle with no fold. Reaching one here
1615 // would be a plan compiled for a program that did not slice, so it becomes an opaque
1616 // node rather than a panic: the engine recomputes and the report says why.
1617 other => self.push(
1618 Op::Pointwise {
1619 code: lam(vec![0], var(0, Ty::unit(), node.span)),
1620 },
1621 vec![self.state],
1622 Some(format!("`{}` is not a view operator", other.name())),
1623 ),
1624 };
1625 self.vertices.insert(id, out);
1626 out
1627 }
1628
1629 /// `f(args…)`, symbolically: inline the function and decompose its body.
1630 ///
1631 /// `scope` is the caller's, needed only for the fallback: a function expression this analysis
1632 /// cannot see into may still *read* variables the plan has operators for, and an opaque node
1633 /// has to take them as inputs rather than leave them unbound.
1634 fn apply(
1635 &mut self,
1636 f: &Core,
1637 args: Vec<OpId>,
1638 scope: &Scope,
1639 ty: Ty,
1640 span: beck_diag::Span,
1641 ) -> OpId {
1642 let Some((params, body)) = self.as_lambda(f) else {
1643 return self.opaque_call(
1644 f,
1645 args,
1646 scope,
1647 ty,
1648 span,
1649 "a view applies a function this analysis cannot see into",
1650 );
1651 };
1652 if params.len() != args.len() {
1653 return self.opaque_call(
1654 f,
1655 args,
1656 scope,
1657 ty,
1658 span,
1659 "a view applies a function to a different number of arguments",
1660 );
1661 }
1662 // The guard goes on here rather than at the call site, because `as_lambda` consults it: a
1663 // definition may not be inlined into its own body, and pushing the name before resolving it
1664 // would refuse the outermost call as well as the recursive one.
1665 let named = match &f.kind {
1666 CoreKind::Global(n) => {
1667 self.inlining.push(n.clone());
1668 true
1669 }
1670 _ => false,
1671 };
1672 let inner: Scope = params.into_iter().zip(args).collect();
1673 let out = self.expr(&body, &inner);
1674 if named {
1675 self.inlining.pop();
1676 }
1677 out
1678 }
1679
1680 /// One operator for a call this analysis will not enter, over the arguments *and* whatever the
1681 /// function expression itself reads.
1682 fn opaque_call(
1683 &mut self,
1684 f: &Core,
1685 args: Vec<OpId>,
1686 scope: &Scope,
1687 ty: Ty,
1688 span: beck_diag::Span,
1689 why: &str,
1690 ) -> OpId {
1691 let mut free = BTreeSet::new();
1692 crate::core::free_vars(f, &mut BTreeSet::new(), &mut free);
1693 let captured: Vec<VarId> = free.into_iter().filter(|v| scope.contains_key(v)).collect();
1694 let base = captured.iter().copied().max().unwrap_or(0) + 1;
1695 let ps: Vec<VarId> = (0..args.len() as VarId).map(|i| base + i).collect();
1696 let call = Core {
1697 kind: CoreKind::App {
1698 func: Box::new(f.clone()),
1699 args: ps.iter().map(|&p| var(p, Ty::unit(), span)).collect(),
1700 },
1701 ty,
1702 tier: Tier::Any,
1703 span,
1704 last_use: false,
1705 order: crate::fields::UNORDERED,
1706 locals: 0,
1707 };
1708 let mut params = captured.clone();
1709 params.extend(ps);
1710 let mut inputs: Vec<OpId> = captured.iter().map(|v| scope[v]).collect();
1711 inputs.extend(args);
1712 self.push(
1713 Op::Pointwise {
1714 code: lam(params, call),
1715 },
1716 inputs,
1717 Some(why.to_string()),
1718 )
1719 }
1720
1721 /// A function expression as parameters and a body, following one level of naming.
1722 fn as_lambda(&self, f: &Core) -> Option<(Vec<VarId>, Core)> {
1723 match &f.kind {
1724 CoreKind::Lam { params, body } => Some((params.to_vec(), (**body).clone())),
1725 CoreKind::Global(name) if !self.inlining.contains(name) => {
1726 let def = self.defs.get(name)?;
1727 match &def.body.kind {
1728 CoreKind::Lam { params, body } => Some((params.to_vec(), (**body).clone())),
1729 _ => None,
1730 }
1731 }
1732 _ => None,
1733 }
1734 }
1735
1736 // ---------------------------------------------------------------------------------------
1737 // The expression
1738 // ---------------------------------------------------------------------------------------
1739
1740 /// Decompose one expression into operators, in the scope of the variables already bound to
1741 /// operators.
1742 fn expr(&mut self, c: &Core, scope: &Scope) -> OpId {
1743 match &c.kind {
1744 CoreKind::Var(v) => match scope.get(v) {
1745 Some(&id) => id,
1746 // A variable bound by something the decomposition did not enter — a `match` arm's
1747 // binder reached through a path that should not exist. Opaque rather than wrong.
1748 None => self.opaque(c, scope, "a variable bound outside the plan"),
1749 },
1750 CoreKind::Const(_) => {
1751 let key = format!("const/{:?}", c.kind);
1752 let id = self.shared(key, Op::Const, Vec::new(), None);
1753 self.constants.entry(id).or_insert_with(|| c.clone());
1754 id
1755 }
1756 CoreKind::Let { var, value, body } => {
1757 let v = self.expr(value, scope);
1758 let mut inner = scope.clone();
1759 inner.insert(*var, v);
1760 self.expr(body, &inner)
1761 }
1762 CoreKind::App { func, args } => {
1763 let ids: Vec<OpId> = args.iter().map(|a| self.expr(a, scope)).collect();
1764 self.apply(func, ids, scope, c.ty.clone(), c.span)
1765 }
1766 CoreKind::Prim { op, args } => self.prim(c, *op, args, scope),
1767 // The two constructs a delta cannot be pushed through: both pick which computation
1768 // runs, and a change to the scrutinee can move the answer between arms.
1769 CoreKind::If { .. } => self.opaque(
1770 c,
1771 scope,
1772 "an `if` picks which computation runs, and a delta can move it between branches",
1773 ),
1774 CoreKind::Match { .. } => self.opaque(
1775 c,
1776 scope,
1777 "a `match` on the input picks which computation runs, and a delta can move it \
1778 between arms",
1779 ),
1780 CoreKind::Lam { .. } => self.opaque(c, scope, "a function used as a value"),
1781 CoreKind::Global(name) => match self.defs.get(name) {
1782 Some(def) if !matches!(def.body.kind, CoreKind::Lam { .. }) => {
1783 let body = def.body.clone();
1784 self.expr(&body, &Scope::new())
1785 }
1786 _ => self.opaque(c, scope, "a definition used as a value"),
1787 },
1788 // Structural constructors are pointwise: a change at an input is a change at the
1789 // output, and there is nothing collection-shaped to maintain.
1790 CoreKind::Make {
1791 ty,
1792 variant,
1793 fields,
1794 } => {
1795 let ids: Vec<OpId> = fields.iter().map(|(_, v)| self.expr(v, scope)).collect();
1796 let ps: Vec<VarId> = (0..fields.len() as VarId).collect();
1797 let code = lam(
1798 ps.clone(),
1799 Core {
1800 kind: CoreKind::Make {
1801 ty: ty.clone(),
1802 variant: variant.clone(),
1803 fields: fields
1804 .iter()
1805 .zip(&ps)
1806 .map(|((n, f), &p)| (n.clone(), var(p, f.ty.clone(), f.span)))
1807 .collect(),
1808 },
1809 ty: c.ty.clone(),
1810 tier: c.tier,
1811 span: c.span,
1812 last_use: false,
1813 // The same field names in the same written order, so the layout the pass
1814 // computed for the literal is the layout of the operator that replaces it.
1815 order: c.order,
1816 locals: 0,
1817 },
1818 );
1819 let names: Vec<&str> = fields.iter().map(|(n, _)| n.as_ref()).collect();
1820 let key = format!("make/{ty}/{variant:?}/{names:?}/{ids:?}");
1821 self.shared(key, Op::Pointwise { code }, ids, None)
1822 }
1823 CoreKind::Field { base, name } => {
1824 let b = self.expr(base, scope);
1825 let code = lam(
1826 vec![0],
1827 Core {
1828 kind: CoreKind::Field {
1829 base: Box::new(var(0, base.ty.clone(), c.span)),
1830 name: name.clone(),
1831 },
1832 ty: c.ty.clone(),
1833 tier: c.tier,
1834 span: c.span,
1835 last_use: false,
1836 order: crate::fields::UNORDERED,
1837 locals: 0,
1838 },
1839 );
1840 self.shared(
1841 format!("field/{name}/{b}"),
1842 Op::Pointwise { code },
1843 vec![b],
1844 None,
1845 )
1846 }
1847 CoreKind::With { base, fields } => {
1848 let mut ids = vec![self.expr(base, scope)];
1849 ids.extend(fields.iter().map(|(_, v)| self.expr(v, scope)));
1850 let ps: Vec<VarId> = (0..ids.len() as VarId).collect();
1851 let code = lam(
1852 ps.clone(),
1853 Core {
1854 kind: CoreKind::With {
1855 base: Box::new(var(0, base.ty.clone(), c.span)),
1856 fields: fields
1857 .iter()
1858 .zip(&ps[1..])
1859 .map(|((n, f), &p)| (n.clone(), var(p, f.ty.clone(), f.span)))
1860 .collect(),
1861 },
1862 ty: c.ty.clone(),
1863 tier: c.tier,
1864 span: c.span,
1865 last_use: false,
1866 order: crate::fields::UNORDERED,
1867 locals: 0,
1868 },
1869 );
1870 self.push(Op::Pointwise { code }, ids, None)
1871 }
1872 CoreKind::ListLit(items) => {
1873 let ids: Vec<OpId> = items.iter().map(|i| self.expr(i, scope)).collect();
1874 let ps: Vec<VarId> = (0..items.len() as VarId).collect();
1875 let code = lam(
1876 ps.clone(),
1877 Core {
1878 kind: CoreKind::ListLit(
1879 items
1880 .iter()
1881 .zip(&ps)
1882 .map(|(i, &p)| var(p, i.ty.clone(), i.span))
1883 .collect(),
1884 ),
1885 ty: c.ty.clone(),
1886 tier: c.tier,
1887 span: c.span,
1888 last_use: false,
1889 order: crate::fields::UNORDERED,
1890 locals: 0,
1891 },
1892 );
1893 let key = format!("list/{ids:?}");
1894 self.shared(key, Op::Pointwise { code }, ids, None)
1895 }
1896 CoreKind::MapLit(pairs) => {
1897 let mut ids = Vec::new();
1898 for (k, v) in pairs {
1899 ids.push(self.expr(k, scope));
1900 ids.push(self.expr(v, scope));
1901 }
1902 let ps: Vec<VarId> = (0..ids.len() as VarId).collect();
1903 let code = lam(
1904 ps.clone(),
1905 Core {
1906 kind: CoreKind::MapLit(
1907 pairs
1908 .iter()
1909 .enumerate()
1910 .map(|(i, (k, v))| {
1911 (
1912 var(ps[i * 2], k.ty.clone(), k.span),
1913 var(ps[i * 2 + 1], v.ty.clone(), v.span),
1914 )
1915 })
1916 .collect(),
1917 ),
1918 ty: c.ty.clone(),
1919 tier: c.tier,
1920 span: c.span,
1921 last_use: false,
1922 order: crate::fields::UNORDERED,
1923 locals: 0,
1924 },
1925 );
1926 self.push(Op::Pointwise { code }, ids, None)
1927 }
1928 }
1929 }
1930
1931 /// A primitive application: a delta operator when there is a rule for it, pointwise otherwise.
1932 fn prim(&mut self, c: &Core, op: Prim, args: &[Core], scope: &Scope) -> OpId {
1933 match (op, args.len()) {
1934 (Prim::MapValues, 1) => {
1935 let m = self.expr(&args[0], scope);
1936 self.shared(format!("map_values/{m}"), Op::MapValues, vec![m], None)
1937 }
1938 (Prim::MapList, 2) | (Prim::FilterList, 2) | (Prim::SortBy, 2) => {
1939 let xs = self.expr(&args[0], scope);
1940 // Only `map_list` becomes a join, and the restriction is about what an arrangement
1941 // holds rather than about what can be recognised. A join's element is a *row* — the
1942 // left value and what it matched — and `map_list` is the one of the three that does
1943 // not keep its element: it stores `f(x)`, which is the same value whether `x`
1944 // arrived alone or in a row. `filter_list` and `sort_by` store the element itself,
1945 // so rewriting either would put rows into the collection its consumers read.
1946 if op == Prim::MapList && self.relate == Relate::Recognise {
1947 match self.joined(xs, &args[1], scope) {
1948 Ok(id) => return id,
1949 Err(why) => {
1950 let f = self.fun(&args[1], scope, &args[0].ty);
1951 let id = self.push(Op::MapList { f }, vec![xs], None);
1952 self.nodes[id].relate = why;
1953 return id;
1954 }
1955 }
1956 }
1957 // A `filter_list` keeps its element, so the sentence above is exactly why it gets
1958 // the *other* binary operator: `Op::Restrict` emits the left element under the left
1959 // key, which is what this node already published (§99.9 item 7).
1960 if op == Prim::FilterList && self.relate == Relate::Recognise {
1961 match self.restricted(xs, &args[1], scope) {
1962 Ok(id) => return id,
1963 Err(why) => {
1964 let f = self.fun(&args[1], scope, &args[0].ty);
1965 let id = self.push(Op::FilterList { f }, vec![xs], None);
1966 self.nodes[id].relate = why;
1967 return id;
1968 }
1969 }
1970 }
1971 let f = self.fun(&args[1], scope, &args[0].ty);
1972 let node = match op {
1973 Prim::MapList => Op::MapList { f },
1974 Prim::FilterList => Op::FilterList { f },
1975 _ => Op::SortBy { f },
1976 };
1977 self.push(node, vec![xs], None)
1978 }
1979 // `concat_lists` takes one argument: a list *of* lists. The `ui:` loop lowering builds
1980 // it as a literal, which is the shape a union of delta streams needs — and the only
1981 // shape a plan can enumerate the inputs of.
1982 (Prim::ConcatLists, 1) => match &args[0].kind {
1983 CoreKind::ListLit(parts) => {
1984 let ids: Vec<OpId> = parts.iter().map(|p| self.expr(p, scope)).collect();
1985 self.push(Op::Concat, ids, None)
1986 }
1987 // Not a literal: a computed collection whose elements are lists, which is what
1988 // `for t in todos:` lowers to. That is a flatten, and a flatten has a delta rule —
1989 // one element's list is replaced, and the rest keep their place because the key
1990 // says where they are.
1991 _ => {
1992 let xs = self.expr(&args[0], scope);
1993 self.shared(format!("flatten/{xs}"), Op::Flatten, vec![xs], None)
1994 }
1995 },
1996 (Prim::ListLen, 1) => {
1997 let xs = self.expr(&args[0], scope);
1998 self.shared(format!("count/{xs}"), Op::Count, vec![xs], None)
1999 }
2000 // A **lowering** rather than a recognition, which is why it takes no [`Relate`] switch
2001 // where the join and the difference do: `list_unique` names the operator, so there is
2002 // no shape being read and no choice being made on the program's behalf. What the
2003 // primitive bought is exactly that — a fold spelling the same thing is opaque, and
2004 // `docs/99` §99.9 item 7 is the argument for giving it a name.
2005 (Prim::ListUnique, 1) => {
2006 let xs = self.expr(&args[0], scope);
2007 self.shared(format!("distinct/{xs}"), Op::Distinct, vec![xs], None)
2008 }
2009 (Prim::ListIsEmpty, 1) => {
2010 let xs = self.expr(&args[0], scope);
2011 self.shared(format!("empty/{xs}"), Op::IsEmpty, vec![xs], None)
2012 }
2013 _ => self.pointwise_prim(c, op, args, scope, None),
2014 }
2015 }
2016
2017 fn pointwise_prim(
2018 &mut self,
2019 c: &Core,
2020 op: Prim,
2021 args: &[Core],
2022 scope: &Scope,
2023 because: Option<String>,
2024 ) -> OpId {
2025 let ids: Vec<OpId> = args.iter().map(|a| self.expr(a, scope)).collect();
2026 let ps: Vec<VarId> = (0..args.len() as VarId).collect();
2027 let code = lam(
2028 ps.clone(),
2029 Core {
2030 kind: CoreKind::Prim {
2031 op,
2032 args: args
2033 .iter()
2034 .zip(&ps)
2035 .map(|(a, &p)| var(p, a.ty.clone(), a.span))
2036 .collect(),
2037 },
2038 ty: c.ty.clone(),
2039 tier: c.tier,
2040 span: c.span,
2041 last_use: false,
2042 order: crate::fields::UNORDERED,
2043 locals: 0,
2044 },
2045 );
2046 let key = format!("prim/{}/{ids:?}", op.name());
2047 self.shared(key, Op::Pointwise { code }, ids, because)
2048 }
2049
2050 /// `map_list(xs, f)` where `f` looks something up, as a join and a loop over its rows.
2051 ///
2052 /// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.6: the loop is
2053 /// not edited and no syntax is added — the operators are emitted for the program that was
2054 /// already there. Per lookup, two nodes: the index — a `map_values` the plan may already have,
2055 /// or an `arrange_by` built for the purpose — and the join, taking the previous join's rows on
2056 /// its left. Then one loop over the last one's rows.
2057 ///
2058 /// Every index here is [`Builder::shared`], so two joins that want the same one get the same
2059 /// node (§99.5 decision 4). For the built ones that needs the key function to be part of the
2060 /// sharing key, and [`crate::relate::fingerprint_fun`] is what makes one into a string — with
2061 /// the key's own parameter written canonically, because `Core` numbers variables per definition
2062 /// and two loops that index the same collection by the same key would otherwise build two
2063 /// identical arrangements.
2064 ///
2065 /// `Err` carries the reason nothing was rewritten, which the caller hangs on the operator that
2066 /// pays for it; `Err(None)` is the ordinary case of a loop that relates nothing.
2067 fn joined(&mut self, xs: OpId, f: &Core, scope: &Scope) -> Result<OpId, Option<String>> {
2068 let known: BTreeSet<VarId> = scope.keys().copied().collect();
2069 let found = match crate::relate::recognise(f, self.defs, &known) {
2070 Ok(found) => found,
2071 Err(crate::relate::Refusal::NoLookup) => return Err(None),
2072 Err(why) => return Err(Some(why.because())),
2073 };
2074
2075 // What the rewritten body still reads, which is the whole point: the capture that made
2076 // every event reconsider every element has to be *gone*, or this buys an index and a second
2077 // arrangement for nothing.
2078 let mut free = BTreeSet::new();
2079 crate::core::free_vars(&found.body, &mut BTreeSet::new(), &mut free);
2080 let kept: Vec<VarId> = free.into_iter().filter(|v| scope.contains_key(v)).collect();
2081 let mut before = BTreeSet::new();
2082 crate::core::free_vars(f, &mut BTreeSet::new(), &mut before);
2083 let was = before.iter().filter(|v| scope.contains_key(v)).count();
2084 if kept.len() >= was {
2085 return Err(Some(crate::relate::Refusal::NothingSaved.because()));
2086 }
2087
2088 let mut left = xs;
2089 for lookup in found.lookups {
2090 let over = self.expr(&lookup.over, scope);
2091 let (index, matched) = match lookup.index {
2092 crate::relate::Index::Unique => (
2093 self.shared(
2094 format!("map_values/{over}"),
2095 Op::MapValues,
2096 vec![over],
2097 None,
2098 ),
2099 Matching::Unique,
2100 ),
2101 crate::relate::Index::Grouped { by, param, answers } => {
2102 let fp = crate::relate::fingerprint_fun(param, &by);
2103 let key = Fun {
2104 code: lam(vec![param], by),
2105 captures: Vec::new(),
2106 };
2107 match answers {
2108 crate::relate::Answers::Rows => (
2109 self.shared(
2110 format!("arrange_by/{over}/{fp}"),
2111 Op::ArrangeBy { key },
2112 vec![over],
2113 None,
2114 ),
2115 Matching::Group,
2116 ),
2117 crate::relate::Answers::Count => (
2118 self.shared(
2119 format!("arrange_by/{over}/{fp}"),
2120 Op::ArrangeBy { key },
2121 vec![over],
2122 None,
2123 ),
2124 Matching::Count,
2125 ),
2126 // An aggregate's right side is not the collection at all: it is one entry
2127 // per group, so the join probes it the way it probes a `map_get`'s map.
2128 // Two aggregates over the same collection, key and projection share one
2129 // node; two that differ in any of the three do not, which is what the
2130 // projection's own fingerprint is in the sharing key for.
2131 crate::relate::Answers::Aggregate(aggregate) => {
2132 let crate::relate::Aggregate {
2133 agg,
2134 of,
2135 param: row,
2136 } = *aggregate;
2137 let name = format!(
2138 "group_by/{}/{over}/{fp}/{}",
2139 agg.name(),
2140 crate::relate::fingerprint_fun(row, &of)
2141 );
2142 let of = Fun {
2143 code: lam(vec![row], of),
2144 captures: Vec::new(),
2145 };
2146 // The one place the aggregates differ downstream, and it is what an
2147 // absent entry *means*: no rows is `None` for an extreme and `0` for
2148 // a total.
2149 let matched = match agg {
2150 Agg::Sum => Matching::Total,
2151 Agg::Min | Agg::Max => Matching::Unique,
2152 };
2153 (
2154 self.shared(name, Op::GroupBy { key, of, agg }, vec![over], None),
2155 matched,
2156 )
2157 }
2158 }
2159 }
2160 };
2161 let key = Fun {
2162 code: lam(vec![lookup.param], lookup.key),
2163 captures: Vec::new(),
2164 };
2165 left = self.push(Op::Join { key, matched }, vec![left, index], None);
2166 }
2167 let join = left;
2168
2169 let mut params = kept.clone();
2170 params.push(found.row);
2171 Ok(self.push(
2172 Op::MapList {
2173 f: Fun {
2174 code: lam(params, found.body),
2175 captures: kept.iter().map(|v| scope[v]).collect(),
2176 },
2177 },
2178 vec![join],
2179 None,
2180 ))
2181 }
2182
2183 /// `filter_list(xs, p)` where `p` asks another collection whether it holds a key, as the
2184 /// difference or the intersection that answers it.
2185 ///
2186 /// [`Op::Restrict`], and §99.9 item 7. Two nodes rather than [`Builder::joined`]'s three per
2187 /// lookup, because there is no loop left over: the operator keeps and drops the elements the
2188 /// filter was keeping and dropping, so nothing downstream has to be re-projected.
2189 ///
2190 /// The index is the same [`Builder::shared`] `map_values` a `map_get` join would build, so a
2191 /// program that both looks a key up and asks whether it exists indexes the collection once —
2192 /// and so do the two halves of a partition, which is the shape `corpus/38-outstanding.beck`
2193 /// carries.
2194 fn restricted(&mut self, xs: OpId, f: &Core, scope: &Scope) -> Result<OpId, Option<String>> {
2195 let known: BTreeSet<VarId> = scope.keys().copied().collect();
2196 let found = match crate::relate::restriction(f, self.defs, &known) {
2197 Ok(found) => found,
2198 // The ordinary case: a filter that relates nothing, which is most of them.
2199 Err(crate::relate::Refusal::NoMembership) => return Err(None),
2200 Err(why) => return Err(Some(why.because())),
2201 };
2202 // [`Builder::joined`]'s rule, and it reads shorter here because there is nothing left to
2203 // capture: the operator's only function is the key and the key reads the element alone. So
2204 // what has to be true is that the predicate captured *something*, and a `map_contains`
2205 // against a constant table is a filter that was already `O(δ)`.
2206 let mut before = BTreeSet::new();
2207 crate::core::free_vars(f, &mut BTreeSet::new(), &mut before);
2208 if !before.iter().any(|v| scope.contains_key(v)) {
2209 return Err(Some(crate::relate::Refusal::NothingSaved.because()));
2210 }
2211 let over = self.expr(&found.over, scope);
2212 let index = self.shared(
2213 format!("map_values/{over}"),
2214 Op::MapValues,
2215 vec![over],
2216 None,
2217 );
2218 let key = Fun {
2219 code: lam(vec![found.param], found.key),
2220 captures: Vec::new(),
2221 };
2222 Ok(self.push(
2223 Op::Restrict {
2224 key,
2225 keep: found.keep,
2226 },
2227 vec![xs, index],
2228 None,
2229 ))
2230 }
2231
2232 /// The per-element function of a collection operator, closed over the operators it reads.
2233 fn fun(&mut self, f: &Core, scope: &Scope, elem_ty: &Ty) -> Fun {
2234 let mut free = BTreeSet::new();
2235 crate::core::free_vars(f, &mut BTreeSet::new(), &mut free);
2236 let captured: Vec<VarId> = free.into_iter().filter(|v| scope.contains_key(v)).collect();
2237 // The element parameter cannot collide with a captured variable, because a captured one is
2238 // free in `f` and this one is bound by the lambda this builds.
2239 let x = captured.iter().copied().max().unwrap_or(0) + 1;
2240 let mut params = captured.clone();
2241 params.push(x);
2242 let call = Core {
2243 kind: CoreKind::App {
2244 func: Box::new(f.clone()),
2245 args: vec![var(x, signal_elem(elem_ty), f.span)],
2246 },
2247 ty: Ty::unit(),
2248 tier: Tier::Any,
2249 span: f.span,
2250 last_use: false,
2251 order: crate::fields::UNORDERED,
2252 locals: 0,
2253 };
2254 Fun {
2255 code: lam(params, call),
2256 captures: captured.iter().map(|v| scope[v]).collect(),
2257 }
2258 }
2259
2260 /// One operator for an expression the decomposition will not enter, over the plan nodes it
2261 /// reads.
2262 fn opaque(&mut self, c: &Core, scope: &Scope, because: &str) -> OpId {
2263 let mut free = BTreeSet::new();
2264 crate::core::free_vars(c, &mut BTreeSet::new(), &mut free);
2265 let params: Vec<VarId> = free.into_iter().filter(|v| scope.contains_key(v)).collect();
2266 let inputs: Vec<OpId> = params.iter().map(|v| scope[v]).collect();
2267 let code = lam(params, c.clone());
2268 self.push(Op::Pointwise { code }, inputs, Some(because.to_string()))
2269 }
2270}
2271
2272// -------------------------------------------------------------------------------------------
2273// Small `Core` constructors
2274// -------------------------------------------------------------------------------------------
2275
2276fn lam(params: Vec<VarId>, body: Core) -> Core {
2277 Core {
2278 ty: Ty::fun(params.iter().map(|_| Ty::unit()).collect(), body.ty.clone()),
2279 tier: body.tier,
2280 span: body.span,
2281 kind: CoreKind::Lam {
2282 params: params.into(),
2283 body: Arc::new(body),
2284 },
2285 last_use: false,
2286 order: crate::fields::UNORDERED,
2287 locals: 0,
2288 }
2289}
2290
2291fn var(v: VarId, ty: Ty, span: beck_diag::Span) -> Core {
2292 Core {
2293 kind: CoreKind::Var(v),
2294 ty,
2295 tier: Tier::Any,
2296 span,
2297 last_use: false,
2298 order: crate::fields::UNORDERED,
2299 locals: 0,
2300 }
2301}
2302
2303fn follow_alias(graph: &Graph, mut id: SigId) -> SigId {
2304 let mut guard = 0;
2305 while matches!(graph.node(id).op, SigOp::Alias) && guard < graph.nodes.len() {
2306 id = graph.node(id).inputs[0];
2307 guard += 1;
2308 }
2309 id
2310}