beck_core/
relate.rs

1//! Recognising the join a loop already contains.
2//!
3//! [`docs/99-the-data-tier-means-of-combination.md`](../../../../../docs/99-the-data-tier-means-of-combination.md)
4//! §99.6:
5//!
6//! > `for x in xs:` whose body contains `map_get(ys, k(x))` **is** an equi-join […] Recognising the
7//! > shape and emitting a `Join` instead of a captured `FlatMap` would make `27-review.beck` and
8//! > `examples/board.beck` faster **with no edit to either program**.
9//!
10//! The cost this removes is not a constant. A per-element function that captured the accumulator is
11//! a *different function* on every event, so [`crate::engine`]'s rebuild rule reapplies it to
12//! every element — a nested-loop join with no index, re-run from scratch per event.
13//! `27-review.beck` is the corpus program that has one, and it did not know it did.
14//!
15//! # What is recognised, stated as the condition rather than as the shape
16//!
17//! One `map_get(m, k)` inside the loop's body, where
18//!
19//! * `m` reads only what the function **captured** — so the collection being looked up in is a node
20//!   the plan already has, or can build, rather than something derived per element; and
21//! * `k` reads only the **element** — so the join key is a function of the left row alone, which is
22//!   what makes it an *equi*-join rather than a predicate.
23//!
24//! Both conditions are about which variables an expression reads, so both survive the lookup being
25//! written behind a call: `27-review`'s is three definitions deep (`verdict_for` → `map_get`), and
26//! §99.6 forecast that as the case inference would fail on. It does not, because the body is
27//! inlined before it is searched — but the *limit* is real and moved rather than removed, and
28//! [`Refusal`] is where it is named.
29//!
30//! # The second shape: a filter that is a lookup into an index nobody built
31//!
32//! One `filter_list(xs, lambda y: g(y) == k(x))` inside the loop's body, where
33//!
34//! * `xs` reads only what the function **captured**, as above;
35//! * `g` reads only the **filtered** element, so it is a key the collection can be arranged by; and
36//! * `k` reads only the **loop's** element, so the probe is a function of the left row alone.
37//!
38//! That is the same equi-join with a different right side. `map_get`'s collection is a `Map` whose
39//! own key *is* the join key, so [`crate::plan::Op::MapValues`]'s arrangement already answers it and
40//! at most one row comes back. A filter's collection is keyed by something else entirely, so the
41//! index has to be built — [`crate::plan::Op::ArrangeBy`], §99.9 item 3 — and several rows share a
42//! key, so what comes back is the **group**.
43//!
44//! The group is the rows the predicate would have kept, in the order the collection held them,
45//! because the index's key is `g(y)` followed by the collection's own key and the probe takes the
46//! range under `g(y)`. That the two agree at all is a fact about `Prim::Eq` rather than a
47//! convention: `==` is [`crate::Value`]'s own total order compared for equality, which is the order
48//! the arrangement is a `BTreeMap` in.
49//!
50//! **What this does not do, stated here because the operator's name promises more.** The group is a
51//! `list`, because the expression it replaced was one and its consumer loops over it. So a row
52//! added to a group rebuilds *that group's* list and no other — the scan over the whole collection
53//! is gone and the capture with it, but the group's own size is still paid. Removing that is
54//! `group by` (§99.9 item 6), which is why item 6 follows this one rather than standing beside it.
55//!
56//! # The third shape, which is the second one asked a different question
57//!
58//! `list_len(filter_list(xs, lambda y: g(y) == k(x)))` is the same equi-join again, and what differs
59//! is only what a probe returns: a number rather than the rows. Nothing about the index changes, so
60//! this is a field of the grouped shape ([`Answers`]) rather than a shape of its own — and it is the
61//! first of §99.9 item 6's aggregates, the one the language already had a spelling for. A group that
62//! is only ever counted is never built, which is what [`crate::plan::Matching::Count`] is for.
63//!
64//! # The fourth shape: a number the group's rows decide
65//!
66//! `list_min(filter_list(…))`, `list_max(…)` and `list_sum(…)`, bare or over a `map_list` of the
67//! same filter, are the same question once more — §99.9 item 6's other three aggregates. What
68//! differs from the count is that the answer is a function of what the rows *say* rather than only
69//! of how many there are, so something has to hold what they contribute:
70//! [`crate::plan::Op::GroupBy`], keyed by the group and holding per group whatever its aggregate
71//! needs — a multiset of the projection, whose two ends are `min` and `max`, or a running total,
72//! which is `sum`.
73//!
74//! It is the one shape whose *index* is not an index. The other three probe an arrangement of the
75//! collection; this probes an arrangement of the **groups**, one entry each. For the extremes the
76//! join above it is a [`crate::plan::Matching::Unique`] — the same probe a `map_get` gets, `Some`
77//! for a group with rows and `None` for one without, which is what `list_min` returns of a list and
78//! of an empty one. For a total it is a [`crate::plan::Matching::Total`], and the difference is
79//! what a *missing* entry means: `list_sum` of no rows is `0`, so the probe answers with a value
80//! where the extremes answer with an absence.
81//!
82//! # The fifth shape, which is not a loop at all
83//!
84//! `filter_list(xs, lambda x: map_contains(m, k(x)))` and its negation are the algebra's
85//! **intersection** and **difference** by key — [`crate::plan::Op::Restrict`], §99.9 item 7 — and
86//! [`restriction`] is where they are read. The conditions are the same two conditions again: `m`
87//! reads only what the function captured, `k` reads only the element.
88//!
89//! What differs is *where* the shape is looked for, and the reason is what comes out of the
90//! operator. A join is recognised at a **site inside a body**, because a loop does other things
91//! besides look up and the body has to be rewritten around the row. A restriction has no body to
92//! rewrite: it keeps and drops the elements the filter was keeping and dropping, so the predicate
93//! is not rewritten, it is *deleted*. That is also why a `filter_list` can have this operator when
94//! it cannot have a join — a join's element is a row, and a filter's consumers read the element.
95//!
96//! The cost it removes is the same one, arrived at from the other side. A predicate that reads a
97//! collection is a different predicate whenever that collection moves, so [`crate::engine`]'s
98//! rebuild rule reconsiders every element on every event — a nested-loop anti-join with no index.
99
100use std::collections::{BTreeMap, BTreeSet};
101use std::sync::Arc;
102
103use beck_diag::Span;
104
105use crate::check::Def;
106use crate::core::{free_vars, Arm, Core, CoreKind, Prim, VarId};
107use crate::plan::{Agg, Presence};
108use crate::ty::{Tier, Ty};
109
110/// The two halves of a joined row, as the field names the rewritten body reads them by.
111///
112/// A record rather than a two-element list because a `Field` is what `Core` already has: no
113/// primitive indexes a list, and inventing one for this would put a form in the language whose only
114/// caller is a rewrite.
115pub const LEFT: &str = "left";
116pub const RIGHT: &str = "right";
117
118/// The type name the joined row carries. Nothing checks it — the plan runs after the checker — but
119/// a value that prints as `Join(left=…, right=…)` in a panic is worth the four bytes.
120pub const ROW: &str = "Join";
121
122/// One lookup, as the join that answers it.
123pub struct Lookup {
124    /// The collection to index, in the caller's variables: the first argument of the `map_get` or
125    /// of the `filter_list`.
126    pub over: Core,
127    /// The join key, over the row the *previous* join in the chain produced — over the element
128    /// itself for the first. This is the `Fun` body of [`crate::plan::Op::Join`].
129    pub key: Core,
130    /// The parameter `key` is written over.
131    pub param: VarId,
132    /// Which index answers it, and therefore what one probe returns.
133    pub index: Index,
134}
135
136/// The index a lookup is answered from — the one difference between the two shapes recognised.
137#[derive(Clone, Debug)]
138pub enum Index {
139    /// `map_get(m, k(x))`: the collection is a `Map` whose own key is the join key, so the index is
140    /// the [`crate::plan::Op::MapValues`] arrangement that already exists and hash-consing shares
141    /// it with every other reader of the same collection. At most one row answers.
142    Unique,
143    /// `filter_list(xs, lambda y: by(y) == k(x))`: nothing keys `xs` by `by`, so the index is an
144    /// [`crate::plan::Op::ArrangeBy`] built for the purpose. Several rows share a key and the group
145    /// answers — either its rows or a question about them.
146    Grouped {
147        /// What the collection is arranged by, as a function of one of its own elements.
148        by: Core,
149        /// The parameter `by` is written over — the filtered element, not the loop's.
150        param: VarId,
151        /// What the body asked the group for.
152        answers: Answers,
153    },
154}
155
156/// What a body wanted from a group, which decides whether the group has to be built at all.
157///
158/// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 6: an
159/// aggregate that builds the collection in order to measure it has done the work the question was
160/// avoiding. All of these are recognised from the same `filter_list` — bare, or wrapped in the
161/// question the program asked of it — which is why they are a field of one variant rather than
162/// several shapes.
163#[derive(Clone, Debug)]
164pub enum Answers {
165    /// The rows. `filter_list(…)` on its own, whose value is a `list`.
166    Rows,
167    /// How many rows. `list_len(filter_list(…))`, whose value is an `Int` — and no list is built.
168    Count,
169    /// A number the group's rows decide, which the group does not have to exist for.
170    /// `list_min(filter_list(…))` or `list_sum(…)`, or either over a `map_list` of it — and no
171    /// list is built.
172    ///
173    /// Boxed because it is the only variant carrying an expression, and an enum is as large as its
174    /// largest arm wherever it is held.
175    Aggregate(Box<Aggregate>),
176}
177
178/// What a group was asked for, and what its rows contribute to the question.
179#[derive(Clone, Debug)]
180pub struct Aggregate {
181    /// Which question.
182    pub agg: Agg,
183    /// What each row contributes, as a function of one row of the *filtered* collection: the
184    /// `map_list`'s function, or the identity when the program asked about the rows themselves.
185    pub of: Core,
186    /// The parameter [`Aggregate::of`] is written over — the filtered element, not the loop's.
187    pub param: VarId,
188}
189
190/// A loop whose body looked things up, taken apart.
191///
192/// # Why this is a list rather than one lookup
193///
194/// A row that shows two related things is an ordinary shape — `corpus/33-awareness.beck` renders a
195/// person's whereabouts *and* their note, so its loop body looks up in two collections — and a rule
196/// that refused it would leave the capture in place and the whole collection reconsidered per event,
197/// which is the cost the operator exists to remove. So every qualifying lookup gets a join, chained:
198/// each takes the previous one's rows on its left, and the row a body finally reads is nested,
199/// `{left: {left: x, right: a₁}, right: a₂}`.
200///
201/// The chain is not free and the cost is memory rather than time: each join holds one row per left
202/// row (§99.5 decision 4), so a body with four lookups arranges the collection four times over. What
203/// it is *not* is the plan choice §99.8 is about — nothing here decides an order, because a lookup
204/// is against an index and there is no side to swap.
205pub struct Recognised {
206    /// One per lookup, in the order the joins are chained.
207    pub lookups: Vec<Lookup>,
208    /// The element parameter the original body was written over.
209    pub elem: VarId,
210    /// The loop body, with each lookup replaced by a read of the row that answers it, over a fresh
211    /// parameter that is the last join's row rather than the left value.
212    pub body: Core,
213    /// The parameter `body` now takes.
214    pub row: VarId,
215}
216
217/// One membership test, as the restriction that answers it.
218///
219/// [`Lookup`]'s sibling, and the difference is what comes out: a lookup produces a *row* and this
220/// produces the element the filter was given, kept or dropped. So there is no rewritten body here
221/// — the operator has no per-element function beyond the key, because a predicate the index
222/// answers is not a predicate any more.
223pub struct Membership {
224    /// The collection whose keys decide, in the caller's variables: the first argument of the
225    /// `map_contains`.
226    pub over: Core,
227    /// The key to probe it by, as a function of the element. This is the `Fun` body of
228    /// [`crate::plan::Op::Restrict`].
229    pub key: Core,
230    /// The parameter `key` is written over — the filtered element.
231    pub param: VarId,
232    /// Which answer keeps the row: the predicate as written, or its negation.
233    pub keep: Presence,
234}
235
236/// Why a body that contained a `map_get` was not recognised as a join.
237///
238/// §99.6's rule for the case inference cannot see: "compile it the slow way and *say so*". These
239/// reach [`crate::plan::Node::because`], so `beck explain cost` prints the reason beside the
240/// operator that pays for it rather than leaving a reader to guess which of the conditions failed.
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub enum Refusal {
243    /// Nothing in the body that could relate: no `map_get` and no `filter_list`.
244    NoLookup,
245    /// The collection looked up in is derived per element, so there is nothing to index once.
246    CollectionReadsTheElement,
247    /// The key reads something other than the element, so it is not an equi-join on the left row.
248    KeyReadsMoreThanTheElement,
249    /// The filter's predicate is not an equality with one side over each element, so there is no
250    /// key to arrange the collection by.
251    PredicateIsNotAnEquality,
252    /// The aggregate's projection reads something other than the row it is applied to, so what
253    /// each row contributes to its group is not a function of that row.
254    ProjectionReadsMoreThanTheRow,
255    /// Recognising it would not remove the capture that costs the rebuild, so it buys nothing.
256    NothingSaved,
257    /// Nothing in the predicate asks another collection whether it holds a key, so there is no
258    /// difference and no intersection here.
259    NoMembership,
260    /// The predicate asks a collection whether it holds a key **and something else besides**, so
261    /// the filter is not the membership test — it contains one.
262    MembershipAndMore,
263}
264
265impl Refusal {
266    /// The sentence `beck explain` prints, in the voice the rest of the plan's reasons use.
267    pub fn because(&self) -> String {
268        match self {
269            Refusal::NoLookup => "its body relates nothing to the collection it loops over".into(),
270            Refusal::CollectionReadsTheElement => {
271                "the collection it looks up in is derived from the element, so there is nothing to \
272                 index once"
273                    .into()
274            }
275            Refusal::KeyReadsMoreThanTheElement => {
276                "the key it looks up by reads more than the element, so it is not an equi-join on \
277                 the left row"
278                    .into()
279            }
280            Refusal::PredicateIsNotAnEquality => {
281                "the predicate it filters by is not an equality between a function of the row and \
282                 a function of the element, so there is no key to arrange the collection by"
283                    .into()
284            }
285            Refusal::ProjectionReadsMoreThanTheRow => {
286                "what it takes the smallest or largest of reads more than the row it is applied \
287                 to, so a group's answer is not a function of the group"
288                    .into()
289            }
290            Refusal::NothingSaved => {
291                "rewriting it against an index would not remove what its function captured, so it \
292                 would cost an index and save nothing"
293                    .into()
294            }
295            Refusal::NoMembership => {
296                "its predicate asks no other collection whether it holds a key".into()
297            }
298            Refusal::MembershipAndMore => {
299                "its predicate asks another collection whether it holds a key and asks something \
300                 else as well, so the filter is not that question — splitting it into two \
301                 operators is a rewrite rather than a reading"
302                    .into()
303            }
304        }
305    }
306}
307
308/// How far a body is inlined before it is searched.
309///
310/// `27-review`'s lookup is two calls deep (`verdict_for`, then the `map_get` in its body) and the
311/// key one more (`payload`). Four is that with room, and it is a *bound* rather than a budget
312/// because the thing it stops is a body that grows exponentially in a chain of calls, not a slow
313/// compile.
314const DEPTH: usize = 4;
315
316/// Try to read a loop's per-element function as a join.
317///
318/// `f` is the function as written — a `Lam` of one parameter, or a global that resolves to one.
319/// `captured` is the set of variables the enclosing plan has operators for, which is what decides
320/// whether the collection being looked up in is something the plan can index.
321pub fn recognise(
322    f: &Core,
323    defs: &BTreeMap<Arc<str>, Def>,
324    captured: &BTreeSet<VarId>,
325) -> Result<Recognised, Refusal> {
326    let (elem, body) = match lambda(f, defs) {
327        Some(pair) => pair,
328        None => return Err(Refusal::NoLookup),
329    };
330    let mut fresh = 1 + max_var(&body).max(captured.iter().copied().max().unwrap_or(0));
331    let body = inline(&body, defs, &mut Vec::new(), &mut fresh, DEPTH);
332
333    let mut sites: Vec<Vec<usize>> = Vec::new();
334    lookups(&body, &mut Vec::new(), &mut sites);
335    if sites.is_empty() {
336        return Err(Refusal::NoLookup);
337    }
338
339    // Each site tested on its own, and the first failure kept only in case *none* qualifies: a body
340    // with one lookup this can index and one it cannot is still worth indexing once.
341    //
342    // Outermost first — [`lookups`] collects in pre-order — and a site under one that **qualified**
343    // is skipped: two chosen sites on one spine would collide under the rewrite, and an aggregate's
344    // own filter is exactly that case. A site under one that *failed* is still considered, which is
345    // the difference between this and skipping every nested site outright: `list_min` over a filter
346    // whose projection reads the loop's element is not an aggregate this can maintain, and the
347    // filter under it is still the group the program would otherwise re-scan.
348    let mut chosen: Vec<(Vec<usize>, Core, Core, Index)> = Vec::new();
349    let mut claimed: Vec<&[usize]> = Vec::new();
350    let mut why = Refusal::NoLookup;
351    for site in &sites {
352        if claimed.iter().any(|outer| site.starts_with(outer)) {
353            continue;
354        }
355        match qualify(&body, site, elem, defs, captured) {
356            Ok((over, key, index)) => {
357                claimed.push(site);
358                chosen.push((site.clone(), over, key, index));
359            }
360            Err(refused) => why = refused,
361        }
362    }
363    if chosen.is_empty() {
364        return Err(why);
365    }
366
367    // The rewrite. Each lookup becomes a read of the row that answers it, and the element becomes a
368    // read through the chain's left spine — `let`s rather than substitutions, so the body is written
369    // once however many times it mentions either.
370    let row = fresh;
371    let n = chosen.len();
372    let answers: Vec<VarId> = (0..n as VarId).map(|k| fresh + 1 + k).collect();
373    let mut rewritten = body;
374    for ((site, _, _, _), &answer) in chosen.iter().zip(&answers) {
375        // Replacing a node with a variable changes no ancestor's arity and no sibling's path, and
376        // the descendants that would have been invalidated were skipped above — so the order these
377        // are applied in does not matter.
378        let ty = follow(&rewritten, site).ty.clone();
379        *follow_mut(&mut rewritten, site) = var(answer, ty, Span::NONE);
380    }
381    for (i, &answer) in answers.iter().enumerate().rev() {
382        rewritten = bind(answer, field_of(spine(row, n - 1 - i), RIGHT), rewritten);
383    }
384    let body = bind(elem, spine(row, n), rewritten);
385
386    // One join per lookup, each keyed over the row the one before it produced. `param` is fresh per
387    // stage because the key function is a `Fun` of its own and its parameter is not the element any
388    // more once there is a stage below it.
389    let lookups: Vec<Lookup> = chosen
390        .into_iter()
391        .enumerate()
392        .map(|(i, (_, over, key, index))| {
393            let param = fresh + 1 + n as VarId + i as VarId;
394            let mut key = key;
395            if i > 0 {
396                substitute(&mut key, elem, &spine(param, i));
397            }
398            Lookup {
399                over,
400                key,
401                param: if i == 0 { elem } else { param },
402                index,
403            }
404        })
405        .collect();
406
407    Ok(Recognised {
408        lookups,
409        elem,
410        body,
411        row,
412    })
413}
414
415/// Try to read a filter's predicate as a **membership test** against another collection.
416///
417/// `filter_list(xs, lambda x: map_contains(m, k(x)))` is the intersection of `xs` with `m`'s keys
418/// and its negation is the difference — [`crate::plan::Op::Restrict`], and
419/// [`docs/99`](../../../../../docs/99-the-data-tier-means-of-combination.md) §99.9 item 7. The two
420/// conditions are [`recognise`]'s, for [`recognise`]'s reasons: `m` may read only what the function
421/// **captured**, so the collection is a node the plan can index once, and `k` may read only the
422/// **element**, so the probe is a function of the row alone.
423///
424/// What it does *not* share with [`recognise`] is the search. A join is recognised at a site
425/// *inside* a body, because a loop does other things as well as look up; a filter's predicate is
426/// the whole of what the operator computes, so a predicate that is a membership test **and
427/// something else** is not this shape at all. Splitting `p(x) and map_contains(m, k(x))` into two
428/// operators is a rewrite the fuser owns rather than a recognition, and it is named as absent in
429/// §99.10 rather than attempted here.
430pub fn restriction(
431    f: &Core,
432    defs: &BTreeMap<Arc<str>, Def>,
433    captured: &BTreeSet<VarId>,
434) -> Result<Membership, Refusal> {
435    let Some((elem, body)) = lambda(f, defs) else {
436        return Err(Refusal::NoMembership);
437    };
438    let mut fresh = 1 + max_var(&body).max(captured.iter().copied().max().unwrap_or(0));
439    let body = inline(&body, defs, &mut Vec::new(), &mut fresh, DEPTH);
440
441    // A predicate that *contains* a membership test without being one is told apart from a
442    // predicate that has nothing to do with one, because the two want opposite things said about
443    // them: the first is a program left at `O(n)` per event by a rewrite this does not do, which
444    // §99.9 item 5 is explicit is not a conservative choice, and the second is every ordinary
445    // filter in the tree.
446    let refuse = |c: &Core| match contains_membership(c, captured) {
447        true => Refusal::MembershipAndMore,
448        false => Refusal::NoMembership,
449    };
450
451    let mut lets = BTreeMap::new();
452    let mut at = peel(&body, &mut lets);
453    // One `not` and no more. Two would cancel, and a predicate written `not not c` is not a shape
454    // worth carrying a loop for — it is refused with the same sentence anything else gets.
455    let mut keep = Presence::In;
456    if let CoreKind::Prim {
457        op: Prim::Not,
458        args,
459    } = &at.kind
460    {
461        if args.len() != 1 {
462            return Err(refuse(&body));
463        }
464        keep = Presence::NotIn;
465        at = peel(&args[0].clone(), &mut lets);
466    }
467    let CoreKind::Prim {
468        op: Prim::MapContains,
469        args,
470    } = &at.kind
471    else {
472        return Err(refuse(&body));
473    };
474    if args.len() != 2 {
475        return Err(refuse(&body));
476    }
477
478    let over = resolve(&args[0], &lets, DEPTH);
479    let reads_over = reads(&over);
480    if reads_over.contains(&elem) || !reads_over.is_subset(captured) {
481        return Err(Refusal::CollectionReadsTheElement);
482    }
483    let key = resolve(&args[1], &lets, DEPTH);
484    if !reads(&key).is_subset(&BTreeSet::from([elem])) {
485        return Err(Refusal::KeyReadsMoreThanTheElement);
486    }
487    Ok(Membership {
488        over,
489        key,
490        param: elem,
491        keep,
492    })
493}
494
495/// Whether an expression asks a collection the plan could index whether it holds a key.
496///
497/// Not "whether there is a `map_contains`" — the collection has to be one the plan already has, or
498/// the answer would be about a map built per element and there would be nothing to index. That is
499/// the same first condition [`qualify`] applies, checked here only to decide which sentence a
500/// refusal carries.
501fn contains_membership(c: &Core, captured: &BTreeSet<VarId>) -> bool {
502    if let CoreKind::Prim {
503        op: Prim::MapContains,
504        args,
505    } = &c.kind
506    {
507        if args.len() == 2 && reads(&args[0]).is_subset(captured) {
508            return true;
509        }
510    }
511    children(c)
512        .into_iter()
513        .any(|child| contains_membership(child, captured))
514}
515
516/// One site, as the index that answers it — or the condition that failed.
517///
518/// The two shapes differ only in where the join key on the right comes from, which is why they are
519/// one function: `map_get` is told the key by the collection it reads, and `filter_list` has to be
520/// read out of an equality. Everything else — that the collection is something the plan already
521/// holds, that the probe is a function of the loop's element alone — is the same condition twice.
522fn qualify(
523    body: &Core,
524    site: &[usize],
525    elem: VarId,
526    defs: &BTreeMap<Arc<str>, Def>,
527    captured: &BTreeSet<VarId>,
528) -> Result<(Core, Core, Index), Refusal> {
529    let at = follow(body, site);
530    // An aggregate's site is the `filter_list` under it, asked a different question. Everything
531    // below reads the filter, and only the answer differs.
532    let (at, asked) = match asked(at) {
533        Some(pair) => pair,
534        None => (at, Asked::Rows),
535    };
536    let CoreKind::Prim { op, args } = &at.kind else {
537        unreachable!("a site is where a `map_get` or a `filter_list` is")
538    };
539    // Resolved against the `let`s the inliner left, because an argument that was not cheap enough
540    // to substitute is bound rather than copied — so the collection may be a variable standing for
541    // one.
542    let mut lets = BTreeMap::new();
543    collect_lets(body, site, &mut lets);
544    let outer = lets.clone();
545
546    let over = resolve(&args[0], &lets, DEPTH);
547    let reads_over = reads(&over);
548    if reads_over.contains(&elem) || !reads_over.is_subset(captured) {
549        return Err(Refusal::CollectionReadsTheElement);
550    }
551    let only = |c: &Core, v: VarId| {
552        let r = reads(c);
553        r.contains(&v) && r.is_subset(&BTreeSet::from([v]))
554    };
555
556    if *op == Prim::MapGet {
557        let key = resolve(&args[1], &lets, DEPTH);
558        if !reads(&key).is_subset(&BTreeSet::from([elem])) {
559            return Err(Refusal::KeyReadsMoreThanTheElement);
560        }
561        return Ok((over, key, Index::Unique));
562    }
563
564    let Some((y, pred)) = lambda(&args[1], defs) else {
565        return Err(Refusal::PredicateIsNotAnEquality);
566    };
567    // A parameter that is also the loop's would make the two sides of the equality
568    // indistinguishable. `Core`'s variables are numbered per definition and the inliner renames
569    // above everything in sight, so this cannot happen — and a rewrite that is wrong about which
570    // element it read would be wrong silently, which is what makes it worth a line.
571    if y == elem {
572        return Err(Refusal::PredicateIsNotAnEquality);
573    }
574    // The predicate's own bindings join the ones in scope at the site: an equality written through
575    // a name is still an equality.
576    let mut inner = lets;
577    let pred = peel(&pred, &mut inner);
578    let CoreKind::Prim {
579        op: Prim::Eq,
580        args: sides,
581    } = &pred.kind
582    else {
583        return Err(Refusal::PredicateIsNotAnEquality);
584    };
585    let left = resolve(&sides[0], &inner, DEPTH);
586    let right = resolve(&sides[1], &inner, DEPTH);
587    // `==` is symmetric and a program may write it either way round, so which side is the index key
588    // is read from what each side *reads* rather than from its position.
589    let (by, key) = if only(&left, y) && only(&right, elem) {
590        (left, right)
591    } else if only(&right, y) && only(&left, elem) {
592        (right, left)
593    } else {
594        return Err(Refusal::PredicateIsNotAnEquality);
595    };
596    let answers = match asked {
597        Asked::Rows => Answers::Rows,
598        Asked::Count => Answers::Count,
599        // The identity, written as the *variable node* the predicate already reads `y` through
600        // rather than as one this function builds: a `Core` carries its type, and the type of the
601        // row is not something the shape of a `filter_list` says.
602        Asked::Aggregated { agg, of: None } => Answers::Aggregate(Box::new(Aggregate {
603            agg,
604            of: find_var(&by, y).ok_or(Refusal::PredicateIsNotAnEquality)?,
605            param: y,
606        })),
607        Asked::Aggregated { agg, of: Some(f) } => {
608            let (z, of) = lambda(f, defs).ok_or(Refusal::ProjectionReadsMoreThanTheRow)?;
609            let of = resolve(&of, &outer, DEPTH);
610            if !reads(&of).is_subset(&BTreeSet::from([z])) {
611                return Err(Refusal::ProjectionReadsMoreThanTheRow);
612            }
613            Answers::Aggregate(Box::new(Aggregate { agg, of, param: z }))
614        }
615    };
616    Ok((
617        over,
618        key,
619        Index::Grouped {
620            by,
621            param: y,
622            answers,
623        },
624    ))
625}
626
627/// What a site asks of a group, before the group's own key is known.
628///
629/// [`Answers`] is the same thing with the projection resolved, which cannot happen until the
630/// filter's parameter has been read out of its predicate — so this carries the *unresolved*
631/// function and [`qualify`] finishes it.
632enum Asked<'a> {
633    Rows,
634    Count,
635    Aggregated {
636        agg: Agg,
637        /// The `map_list`'s function, or `None` for an aggregate of the rows themselves.
638        of: Option<&'a Core>,
639    },
640}
641
642/// The `filter_list` under an aggregate, and the question the aggregate asked of it.
643///
644/// `None` for anything that is not one, which is what keeps [`lookups`] and [`qualify`] agreeing
645/// about what a site is: a site is a `map_get`, a `filter_list`, or a node this function reads.
646///
647/// The wrappers are alternatives rather than a chain, so they are listed here rather than peeled in
648/// a loop, and a reader looking for "which shapes count as an aggregate" finds them in one place.
649fn asked(c: &Core) -> Option<(&Core, Asked<'_>)> {
650    let CoreKind::Prim { op, args } = &c.kind else {
651        return None;
652    };
653    match op {
654        Prim::ListLen if args.len() == 1 && is_filter(&args[0]) => Some((&args[0], Asked::Count)),
655        Prim::ListMin | Prim::ListMax | Prim::ListSum if args.len() == 1 => {
656            let agg = match op {
657                Prim::ListMin => Agg::Min,
658                Prim::ListMax => Agg::Max,
659                _ => Agg::Sum,
660            };
661            if is_filter(&args[0]) {
662                return Some((&args[0], Asked::Aggregated { agg, of: None }));
663            }
664            // `list_min(map_list(filter_list(…), f))` — the aggregate of what each row projects
665            // to, which is how anybody writes "the earliest of their deadlines" or "what they are
666            // owed" rather than "the smallest of their rows".
667            let CoreKind::Prim {
668                op: Prim::MapList,
669                args: mapped,
670            } = &args[0].kind
671            else {
672                return None;
673            };
674            match mapped.len() == 2 && is_filter(&mapped[0]) {
675                true => Some((
676                    &mapped[0],
677                    Asked::Aggregated {
678                        agg,
679                        of: Some(&mapped[1]),
680                    },
681                )),
682                false => None,
683            }
684        }
685        _ => None,
686    }
687}
688
689/// Whether this node is `filter_list(xs, p)`.
690fn is_filter(c: &Core) -> bool {
691    matches!(
692        &c.kind,
693        CoreKind::Prim { op: Prim::FilterList, args } if args.len() == 2
694    )
695}
696
697/// The first node in an expression that reads `v`, so a variable can be recovered with the type it
698/// was written with.
699fn find_var(c: &Core, v: VarId) -> Option<Core> {
700    if matches!(&c.kind, CoreKind::Var(id) if *id == v) {
701        return Some(c.clone());
702    }
703    children(c).into_iter().find_map(|child| find_var(child, v))
704}
705
706/// An expression with its leading `let`s taken off and remembered.
707fn peel(c: &Core, lets: &mut BTreeMap<VarId, Core>) -> Core {
708    match &c.kind {
709        CoreKind::Let { var, value, body } => {
710            lets.insert(*var, (**value).clone());
711            peel(body, lets)
712        }
713        _ => c.clone(),
714    }
715}
716
717// -------------------------------------------------------------------------------------------
718// Reading a function
719// -------------------------------------------------------------------------------------------
720
721/// A one-parameter function as its parameter and its body, following one level of naming.
722fn lambda(f: &Core, defs: &BTreeMap<Arc<str>, Def>) -> Option<(VarId, Core)> {
723    match &f.kind {
724        CoreKind::Lam { params, body } if params.len() == 1 => Some((params[0], (**body).clone())),
725        CoreKind::Global(name) => match &defs.get(name)?.body.kind {
726            CoreKind::Lam { params, body } if params.len() == 1 => {
727                Some((params[0], (**body).clone()))
728            }
729            _ => None,
730        },
731        _ => None,
732    }
733}
734
735/// Every `map_get` and every `filter_list` in an expression, as the path of child indices that
736/// reaches it.
737///
738/// A path rather than a pointer because the rewrite happens afterwards and has to reach the same
739/// node in a `&mut` walk; `Core` is a tree of boxes, so there is no id to hold on to.
740///
741/// A site inside a nested `lambda` is not found, because [`children`] does not enter one: a lookup
742/// there is a lookup per *call* of that function rather than per element.
743///
744/// An aggregate counts as a site only when the `filter_list` it measures is *syntactically* under
745/// it ([`asked`]). That is not a shortcut, it is what keeps the aggregate from swallowing the
746/// group: a site inside another one is skipped, so admitting every `list_len` would hide the
747/// `filter_list` under it behind an outer site that could not qualify. Written this way, the outer
748/// site exists exactly when the inner one would have qualified too. What it costs is an aggregate
749/// written through a `let` — `g = filter_list(…)` then `list_len(g)` — which is recognised as the
750/// group rather than as the question, and is slower rather than wrong.
751fn lookups(c: &Core, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
752    if matches!(
753        &c.kind,
754        CoreKind::Prim {
755            op: Prim::MapGet | Prim::FilterList,
756            args
757        } if args.len() == 2
758    ) || asked(c).is_some()
759    {
760        out.push(path.clone());
761    }
762    for (i, child) in children(c).into_iter().enumerate() {
763        path.push(i);
764        lookups(child, path, out);
765        path.pop();
766    }
767}
768
769/// The `let` bindings that are in scope at a path, so an expression under it can be resolved.
770fn collect_lets(c: &Core, path: &[usize], out: &mut BTreeMap<VarId, Core>) {
771    let Some((&step, rest)) = path.split_first() else {
772        return;
773    };
774    if let CoreKind::Let { var, value, .. } = &c.kind {
775        // Child 1 is the body: a binding is in scope there and not in its own value.
776        if step == 1 {
777            out.insert(*var, (**value).clone());
778        }
779    }
780    if let Some(child) = children(c).into_iter().nth(step) {
781        collect_lets(child, rest, out);
782    }
783}
784
785/// An expression with its `let`-bound variables expanded, so that what it *reads* is what it
786/// really reads rather than what the inliner named.
787fn resolve(c: &Core, lets: &BTreeMap<VarId, Core>, depth: usize) -> Core {
788    if depth == 0 {
789        return c.clone();
790    }
791    if let CoreKind::Var(v) = &c.kind {
792        if let Some(bound) = lets.get(v) {
793            return resolve(bound, lets, depth - 1);
794        }
795        return c.clone();
796    }
797    let mut out = c.clone();
798    for child in children_mut(&mut out) {
799        *child = resolve(child, lets, depth);
800    }
801    out
802}
803
804fn reads(c: &Core) -> BTreeSet<VarId> {
805    let mut out = BTreeSet::new();
806    free_vars(c, &mut BTreeSet::new(), &mut out);
807    out
808}
809
810// -------------------------------------------------------------------------------------------
811// Inlining, so that a lookup written behind a call is still a lookup
812// -------------------------------------------------------------------------------------------
813
814/// Inline the calls a search would otherwise have to see through.
815///
816/// Two rules, and the second is what keeps this from changing what the program means:
817///
818/// * a callee's body is **α-renamed** above every variable in sight before it is used, so nothing
819///   it binds can capture what the caller passed;
820/// * an argument is **substituted** only when it is cheap and cannot fail — a variable, a constant,
821///   a field path over those — and is otherwise bound with a `let`. Substituting a call would
822///   evaluate it once per mention and *not at all* when the parameter is unused, and a view may
823///   raise, so "pure" is not on its own enough to make copying an argument free.
824fn inline(
825    c: &Core,
826    defs: &BTreeMap<Arc<str>, Def>,
827    stack: &mut Vec<Arc<str>>,
828    fresh: &mut VarId,
829    depth: usize,
830) -> Core {
831    if depth == 0 {
832        return c.clone();
833    }
834    let mut out = c.clone();
835    for child in children_mut(&mut out) {
836        *child = inline(child, defs, stack, fresh, depth);
837    }
838    let CoreKind::App { func, args } = &out.kind else {
839        return out;
840    };
841    let (params, body, named) = match &func.kind {
842        CoreKind::Lam { params, body } => (params.to_vec(), (**body).clone(), None),
843        CoreKind::Global(name) if !stack.contains(name) => match defs.get(name) {
844            Some(def) => match &def.body.kind {
845                CoreKind::Lam { params, body } => {
846                    (params.to_vec(), (**body).clone(), Some(name.clone()))
847                }
848                _ => return out,
849            },
850            None => return out,
851        },
852        _ => return out,
853    };
854    if params.len() != args.len() {
855        return out;
856    }
857
858    let offset = *fresh;
859    let mut body = body;
860    let top = max_var(&body);
861    rename(&mut body, offset);
862    *fresh = offset + top + 1;
863
864    let mut bound = body;
865    for (p, arg) in params.iter().zip(args).rev() {
866        let p = p + offset;
867        if simple(arg) {
868            substitute(&mut bound, p, arg);
869        } else {
870            bound = bind(p, arg.clone(), bound);
871        }
872    }
873    if let Some(name) = named {
874        stack.push(name);
875        let deeper = inline(&bound, defs, stack, fresh, depth - 1);
876        stack.pop();
877        return deeper;
878    }
879    inline(&bound, defs, stack, fresh, depth - 1)
880}
881
882/// Whether copying an expression is free: it cannot fail, cannot allocate a call frame, and reading
883/// it twice costs what reading it once did.
884fn simple(c: &Core) -> bool {
885    match &c.kind {
886        CoreKind::Var(_) | CoreKind::Const(_) | CoreKind::Global(_) => true,
887        CoreKind::Field { base, .. } => simple(base),
888        _ => false,
889    }
890}
891
892/// Shift every variable an expression binds or reads, so a callee's body cannot capture a caller's.
893fn rename(c: &mut Core, by: VarId) {
894    match &mut c.kind {
895        CoreKind::Var(v) => *v += by,
896        CoreKind::Lam { params, body } => {
897            *params = params.iter().map(|p| p + by).collect();
898            let mut inner = (**body).clone();
899            rename(&mut inner, by);
900            *body = Arc::new(inner);
901            return;
902        }
903        CoreKind::Let { var, .. } => *var += by,
904        CoreKind::Match { arms, .. } => {
905            for arm in arms.iter_mut() {
906                rename_pattern(&mut arm.pattern, by);
907            }
908        }
909        _ => {}
910    }
911    for child in children_mut(c) {
912        rename(child, by);
913    }
914}
915
916fn rename_pattern(p: &mut crate::core::Pattern, by: VarId) {
917    use crate::core::Pattern;
918    match p {
919        Pattern::Wildcard | Pattern::Const(_) => {}
920        Pattern::Bind(v) => *v += by,
921        Pattern::At { var, inner } => {
922            *var += by;
923            rename_pattern(inner, by);
924        }
925        Pattern::Ctor { binds, .. } => binds.iter_mut().for_each(|(_, p)| rename_pattern(p, by)),
926        Pattern::Or(alts) => alts.iter_mut().for_each(|p| rename_pattern(p, by)),
927        Pattern::List { items, rest } => {
928            items.iter_mut().for_each(|p| rename_pattern(p, by));
929            if let Some(Some(v)) = rest {
930                *v += by;
931            }
932        }
933    }
934}
935
936/// Replace a variable with an expression, stopping wherever the variable is rebound.
937///
938/// The rebinding check is not defensive: [`rename`] has already made a collision impossible for the
939/// callers here, and it is written anyway because a substitution that is wrong about scope is wrong
940/// silently and only on a program that shadows.
941fn substitute(c: &mut Core, v: VarId, to: &Core) {
942    match &mut c.kind {
943        CoreKind::Var(x) if *x == v => {
944            let ty = c.ty.clone();
945            let span = c.span;
946            *c = to.clone();
947            c.ty = ty;
948            c.span = span;
949            return;
950        }
951        CoreKind::Lam { params, body } => {
952            if params.contains(&v) {
953                return;
954            }
955            let mut inner = (**body).clone();
956            substitute(&mut inner, v, to);
957            *body = Arc::new(inner);
958            return;
959        }
960        CoreKind::Let { var, value, body } => {
961            substitute(value, v, to);
962            if *var != v {
963                substitute(body, v, to);
964            }
965            return;
966        }
967        CoreKind::Match { scrutinee, arms } => {
968            substitute(scrutinee, v, to);
969            for arm in arms.iter_mut() {
970                if arm.pattern.binders().contains(&v) {
971                    continue;
972                }
973                for e in arm.exprs_mut() {
974                    substitute(e, v, to);
975                }
976            }
977            return;
978        }
979        _ => {}
980    }
981    for child in children_mut(c) {
982        substitute(child, v, to);
983    }
984}
985
986/// An expression's shape as a string, so two that are the same expression share one index.
987///
988/// The plan's hash-consing keys on a string, and the collection alone is not enough for
989/// `arrange_by`: two joins over one collection by *different* keys are two indexes, and two by the
990/// same key are one. `Core` is not `Eq`, and the parts of it that are not the expression — spans,
991/// and the annotations [`crate::liveness`], [`crate::fields`] and [`crate::frames`] leave — would
992/// make two identical expressions look different, so this writes down what a reader would call the
993/// expression and nothing else.
994///
995/// Being wrong in the safe direction costs an index rather than an answer: two fingerprints that
996/// differ where the expressions agree build two indexes that hold the same thing.
997pub fn fingerprint(c: &Core) -> String {
998    let mut out = String::new();
999    write_fingerprint(c, None, &mut out);
1000    out
1001}
1002
1003/// The same, for an expression written over one **bound** parameter, whose number is written
1004/// canonically.
1005///
1006/// This is the form every index's key takes, and it is a separate function because the difference
1007/// is not cosmetic: `lambda b: b.lot` and `lambda c: c.lot` are the same key, and `Core` numbers
1008/// variables per definition, so two loops that index the same collection by the same key arrive
1009/// here with different numbers. Fingerprinting them apart built **two identical arrangements** — an
1010/// arrangement is memory per subscriber as well as work per event
1011/// ([`docs/23`](../../../../../docs/23-incremental-views-report.md) §23.14), so the safe direction
1012/// was not free.
1013///
1014/// Only the parameter is normalised, and that is enough because an index key **reads nothing else**
1015/// — the recogniser refuses the shape otherwise. A `let` bound inside one would still fingerprint
1016/// by its number, which is the same conservatism one level down and has no program.
1017pub fn fingerprint_fun(param: VarId, body: &Core) -> String {
1018    let mut out = String::new();
1019    write_fingerprint(body, Some(param), &mut out);
1020    out
1021}
1022
1023fn write_fingerprint(c: &Core, param: Option<VarId>, out: &mut String) {
1024    use std::fmt::Write;
1025    match &c.kind {
1026        CoreKind::Const(v) => {
1027            let _ = write!(out, "c{v:?}");
1028        }
1029        CoreKind::Var(v) => match param == Some(*v) {
1030            true => out.push_str("v_"),
1031            false => {
1032                let _ = write!(out, "v{v}");
1033            }
1034        },
1035        CoreKind::Global(n) => {
1036            let _ = write!(out, "g{n}");
1037        }
1038        CoreKind::Prim { op, .. } => {
1039            let _ = write!(out, "p{}", op.name());
1040        }
1041        CoreKind::Field { name, .. } => {
1042            let _ = write!(out, "f{name}");
1043        }
1044        CoreKind::Make { ty, variant, .. } => {
1045            let _ = write!(out, "m{ty}.{}", variant.as_deref().unwrap_or(""));
1046        }
1047        CoreKind::Lam { params, body } => {
1048            let _ = write!(out, "l{params:?}");
1049            write_fingerprint(body, param, out);
1050        }
1051        CoreKind::Let { var, .. } => {
1052            let _ = write!(out, "b{var}");
1053        }
1054        CoreKind::App { .. } => out.push('a'),
1055        CoreKind::If { .. } => out.push('i'),
1056        CoreKind::Match { .. } => out.push('s'),
1057        CoreKind::With { fields, .. } => {
1058            let _ = write!(
1059                out,
1060                "w{:?}",
1061                fields.iter().map(|(n, _)| n).collect::<Vec<_>>()
1062            );
1063        }
1064        CoreKind::ListLit(_) => out.push('['),
1065        CoreKind::MapLit(_) => out.push('{'),
1066    }
1067    out.push('(');
1068    for child in children(c) {
1069        write_fingerprint(child, param, out);
1070        out.push(',');
1071    }
1072    out.push(')');
1073}
1074
1075// -------------------------------------------------------------------------------------------
1076// Walking a `Core` by position
1077// -------------------------------------------------------------------------------------------
1078
1079/// Every subexpression, in the order a path indexes them.
1080///
1081/// One function paired with [`children_mut`], and they must agree: a path found by the first is
1082/// followed by the second, so a kind listed in one and not the other would rewrite the wrong node.
1083fn children(c: &Core) -> Vec<&Core> {
1084    match &c.kind {
1085        CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => Vec::new(),
1086        // A `Lam`'s body is behind an `Arc`, so it is not reachable as a `&mut` child. Nothing
1087        // below one is searched for a lookup: a lookup inside a nested function is a lookup per
1088        // *call* of that function, which is not the shape this recognises.
1089        CoreKind::Lam { .. } => Vec::new(),
1090        CoreKind::App { func, args } => {
1091            let mut out = vec![&**func];
1092            out.extend(args.iter());
1093            out
1094        }
1095        CoreKind::Prim { args, .. } => args.iter().collect(),
1096        CoreKind::Let { value, body, .. } => vec![&**value, &**body],
1097        CoreKind::If { cond, then, alt } => vec![&**cond, &**then, &**alt],
1098        CoreKind::Match { scrutinee, arms } => {
1099            let mut out = vec![&**scrutinee];
1100            out.extend(arms.iter().flat_map(Arm::exprs));
1101            out
1102        }
1103        CoreKind::Make { fields, .. } => fields.iter().map(|(_, v)| v).collect(),
1104        CoreKind::Field { base, .. } => vec![&**base],
1105        CoreKind::With { base, fields } => {
1106            let mut out = vec![&**base];
1107            out.extend(fields.iter().map(|(_, v)| v));
1108            out
1109        }
1110        CoreKind::ListLit(items) => items.iter().collect(),
1111        CoreKind::MapLit(pairs) => pairs.iter().flat_map(|(k, v)| [k, v]).collect(),
1112    }
1113}
1114
1115fn children_mut(c: &mut Core) -> Vec<&mut Core> {
1116    match &mut c.kind {
1117        CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => Vec::new(),
1118        CoreKind::Lam { .. } => Vec::new(),
1119        CoreKind::App { func, args } => {
1120            let mut out = vec![&mut **func];
1121            out.extend(args.iter_mut());
1122            out
1123        }
1124        CoreKind::Prim { args, .. } => args.iter_mut().collect(),
1125        CoreKind::Let { value, body, .. } => vec![&mut **value, &mut **body],
1126        CoreKind::If { cond, then, alt } => vec![&mut **cond, &mut **then, &mut **alt],
1127        CoreKind::Match { scrutinee, arms } => {
1128            let mut out = vec![&mut **scrutinee];
1129            out.extend(arms.iter_mut().flat_map(Arm::exprs_mut));
1130            out
1131        }
1132        CoreKind::Make { fields, .. } => fields.iter_mut().map(|(_, v)| v).collect(),
1133        CoreKind::Field { base, .. } => vec![&mut **base],
1134        CoreKind::With { base, fields } => {
1135            let mut out = vec![&mut **base];
1136            out.extend(fields.iter_mut().map(|(_, v)| v));
1137            out
1138        }
1139        CoreKind::ListLit(items) => items.iter_mut().collect(),
1140        CoreKind::MapLit(pairs) => pairs.iter_mut().flat_map(|(k, v)| [k, v]).collect(),
1141    }
1142}
1143
1144fn follow<'a>(c: &'a Core, path: &[usize]) -> &'a Core {
1145    match path.split_first() {
1146        None => c,
1147        Some((&i, rest)) => follow(children(c).swap_remove(i), rest),
1148    }
1149}
1150
1151fn follow_mut<'a>(c: &'a mut Core, path: &[usize]) -> &'a mut Core {
1152    match path.split_first() {
1153        None => c,
1154        Some((&i, rest)) => follow_mut(children_mut(c).swap_remove(i), rest),
1155    }
1156}
1157
1158/// The highest variable an expression names, so a fresh one can be chosen above it.
1159///
1160/// It descends into a `Lam`'s body, which [`children`] deliberately does not: a variable that only
1161/// a nested function binds is still a variable a rename would collide with.
1162fn max_var(c: &Core) -> VarId {
1163    let mut top = match &c.kind {
1164        CoreKind::Var(v) => *v,
1165        CoreKind::Let { var, .. } => *var,
1166        CoreKind::Lam { params, body } => {
1167            max_var(body).max(params.iter().copied().max().unwrap_or(0))
1168        }
1169        CoreKind::Match { arms, .. } => arms
1170            .iter()
1171            .filter_map(|a| a.pattern.binders().into_iter().max())
1172            .max()
1173            .unwrap_or(0),
1174        _ => 0,
1175    };
1176    for child in children(c) {
1177        top = top.max(max_var(child));
1178    }
1179    top
1180}
1181
1182// -------------------------------------------------------------------------------------------
1183// Small `Core` constructors
1184// -------------------------------------------------------------------------------------------
1185
1186fn var(v: VarId, ty: Ty, span: Span) -> Core {
1187    Core {
1188        kind: CoreKind::Var(v),
1189        ty,
1190        tier: Tier::Any,
1191        span,
1192        last_use: false,
1193        order: crate::fields::UNORDERED,
1194        locals: 0,
1195    }
1196}
1197
1198fn field_of(base: Core, name: &str) -> Core {
1199    Core {
1200        kind: CoreKind::Field {
1201            base: Box::new(base),
1202            name: Arc::from(name),
1203        },
1204        ty: Ty::unit(),
1205        tier: Tier::Any,
1206        span: Span::NONE,
1207        last_use: false,
1208        order: crate::fields::UNORDERED,
1209        locals: 0,
1210    }
1211}
1212
1213/// A row's **left spine**: `row.left.left…`, `depth` steps up the chain of joins.
1214///
1215/// Stage `i`'s row holds stage `i - 1`'s row on its left and stage `i`'s answer on its right, so
1216/// walking `depth` steps left from the last row is how the body reaches an earlier stage's answer —
1217/// and walking all the way is how it reaches the element the loop was written over.
1218fn spine(v: VarId, depth: usize) -> Core {
1219    let mut out = var(v, Ty::unit(), Span::NONE);
1220    for _ in 0..depth {
1221        out = field_of(out, LEFT);
1222    }
1223    out
1224}
1225
1226fn bind(v: VarId, value: Core, body: Core) -> Core {
1227    Core {
1228        ty: body.ty.clone(),
1229        tier: body.tier,
1230        span: body.span,
1231        kind: CoreKind::Let {
1232            var: v,
1233            value: Box::new(value),
1234            body: Box::new(body),
1235        },
1236        last_use: false,
1237        order: crate::fields::UNORDERED,
1238        locals: 0,
1239    }
1240}