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}