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}