Skip to main content

zelkova_compiler/ir/
decision.rs

1//! Lowers a `case`'s branches into an explicit decision tree, so a backend walks one
2//! rather than re-deriving, at emission time, which test distinguishes which branch and
3//! what order they run in (`GEN-5`; see `docs/tickets/README.md`'s tombstone row).
4//!
5//! [`build`] is the pass. It reads a `case`'s branches exactly as
6//! [`super::TypedTermKind::Case`] carries them — a `Vec<(TermPattern, Box<TypedTerm>)>`
7//! in source order — and answers with the [`Decision`] a backend walks instead.
8//! `TypedTermKind::Case`'s own shape does not change: the typer still wants a flat list,
9//! to generate one set of constraints per branch, all against the same scrutinee type,
10//! with an order that does not matter to it. This is a second, additional view of the
11//! same branches, built once a backend is ready to emit them.
12//!
13//! # A chain, not a table
14//!
15//! [Conditional evaluation](../../../docs/spec/evaluation-semantics.md#conditional-evaluation)
16//! tries a `case`'s branches in the order written and commits to the first that
17//! matches, never another. [`build`] follows that literally. A [`Decision::Test`] checks
18//! one value against one [`Outcome`]: `matched` is the rest of the same branch, and
19//! `default` is the tree for every branch after it — a chain, not a table grouping every
20//! branch that tests the same value into one node. A backend that wants a `switch` can
21//! flatten a run of `Test`s on the same occurrence itself.
22//!
23//! Two branches naming the same constructor, `Cons a _ -> a` then `Cons _ b -> b`, each
24//! get a `Test` of their own, the second in the first's `default`. The second is dead —
25//! any `Cons` value takes the first — but it is still built: telling that it is dead is
26//! coverage checking's question (`LANG-19`), not this pass's.
27//!
28//! # Recursive over a pattern's sub-patterns
29//!
30//! A branch's pattern is lowered by walking it, and then its sub-patterns, depth first
31//! and left to right, each at the [`Occurrence`] that leads to it from the scrutinee. A
32//! wildcard tests nothing and binds nothing, and neither does `()`, whose type has
33//! one value, nor a constructor that did not resolve, which has no case to test for and
34//! is never emitted. A variable binds the value at its occurrence. A tuple tests nothing,
35//! since a value of a tuple type is always a tuple, and goes on to its elements; a record
36//! pattern tests nothing either, a record having one shape, and goes on to each entry's
37//! field by its label, in the order the entries were written. A
38//! literal or a constructor is a `Test` at its occurrence; a constructor then goes on
39//! to its arguments. Every `Test` on the way down falls back, as its `default`, to the
40//! tree for the branches after this one: a pattern that fails part-way through fails as
41//! a whole.
42//!
43//! That fallback tree is copied into each such `default` rather than shared, since a
44//! [`Decision`] is a tree and not a graph. A branch with `k` refutable parts (each
45//! constructor or literal anywhere in its pattern) holds `k` copies of the tree for the
46//! branches after it, and those branches were built the same way, so the tree's size is
47//! the *product* over the branches of each one's `k`, not the sum. A pattern that tests
48//! at most one value, as every branch did while a sub-pattern below the top could only
49//! be irrefutable, keeps that linear. A `case` over a tuple of constructors does not:
50//! eight branches over `(Flag, Flag, Flag)` come to 3^8 copies of the final `Fail` and
51//! 2.2 MB of JavaScript, and sixteen over a `(Msg, State)` pair to 2^16 copies and about
52//! 40 MB. Nothing bounds it or reports it. `GEN-26` tracks the fix; this pass does not
53//! do it.
54//!
55//! A sub-pattern may be any pattern, so a constructor or a literal nested inside another
56//! pattern is a `Test` below [`Occurrence::Root`], and a binding may sit any number of
57//! steps down. `crates/zelkova-compiler/tests/ir.rs` pins the tree built from source for
58//! one; this module's own tests build the patterns by hand, to pin the walk alone.
59
60use crate::name::Name;
61
62use super::{Constructor, LiteralValue, TermPattern, TermPatternKind, Type, TypedTerm};
63
64/// A path to one value reached from a `case`'s scrutinee: the scrutinee itself, or one
65/// step inside another occurrence. A pattern nested `n` deep is reached by a path of `n`
66/// [`Step`]s.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Occurrence {
69    /// The scrutinee itself.
70    Root,
71    /// The value one step inside another occurrence.
72    At(Box<Occurrence>, Step),
73}
74
75impl Occurrence {
76    /// The occurrence one step further in: `self` is a constructor, a tuple or a record,
77    /// and `step` says which of its positions.
78    pub fn field(&self, step: Step) -> Occurrence {
79        Occurrence::At(Box::new(self.clone()), step)
80    }
81}
82
83/// One step of an [`Occurrence`]: which sub-value of a constructor, a tuple or a record.
84/// A constructor's argument and a tuple's element are counted from zero the way
85/// [`Constructor::index`] counts a case; a record's field is named by its label.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum Step {
88    ConstructorArgument(usize),
89    TupleElement(usize),
90    /// The field of a record with this label: where a [record
91    /// pattern](super::TermPatternKind::Record)'s entry is matched.
92    ///
93    /// By label and not by position, since a record type is a set of fields with no
94    /// order of its own (`typer::Type::Record`); the `ir` module's doc comment, *What
95    /// this shape owes WebAssembly*, says which order a positional target reads them in.
96    /// Reading one is a backend's to emit: `zelkova_js::emit` writes it as a property read.
97    Field(Name),
98}
99
100/// The value a [`Decision::Test`] checks for.
101#[derive(Debug, Clone, PartialEq)]
102pub enum Outcome {
103    /// The value was built by this constructor.
104    Constructor(Constructor),
105    /// The value equals this `Int`, `Float`, `Char`, `String` or `Bool`.
106    Literal(LiteralValue),
107}
108
109/// One name a [`Decision::Leaf`] binds: the value at `occurrence`, of type `tpe`.
110///
111/// The type is the solved one the pattern carried. It is here because a backend with
112/// more than one representation — WebAssembly's `i64`, `f64` and reference — reads
113/// which one a value has off its type, and the leaf gives it no other way back to that
114/// type short of working it out again from the scrutinee's.
115#[derive(Debug, Clone, PartialEq)]
116pub struct Binding {
117    pub name: String,
118    pub occurrence: Occurrence,
119    pub tpe: Type,
120}
121
122/// The tree a `case`'s branches lower to: what test distinguishes which branch, in what
123/// order they are tried, and the names each leaf binds.
124///
125/// `'a` is the lifetime of the [`TypedTerm`] the branches were built from: a leaf
126/// borrows its branch's body rather than cloning it, since [`TypedTerm`] carries no
127/// `Clone` impl. A body can appear in more than one leaf once a fallback tree is copied
128/// (see this module's doc comment), which a borrow makes free in the IR; a backend that
129/// writes each leaf out writes the body once per leaf (`GEN-26`).
130///
131/// Two trees are equal when they have the same shape and each pair of leaves borrows
132/// the *same* body — compared by address, since [`TypedTerm`] has no `PartialEq`. That
133/// is the equality a test wants: the leaf is the one built from this branch.
134#[derive(Debug, Clone)]
135pub enum Decision<'a> {
136    /// Check the value at `scrutinee` against `outcome`: `matched` if it is that value,
137    /// `default` if it is not.
138    Test {
139        scrutinee: Occurrence,
140        outcome: Outcome,
141        matched: Box<Decision<'a>>,
142        default: Box<Decision<'a>>,
143    },
144    /// A branch matched: its bindings, each already named and given the occurrence to
145    /// read it from, and its body.
146    Leaf {
147        bindings: Vec<Binding>,
148        body: &'a TypedTerm,
149    },
150    /// No branch matches.
151    ///
152    /// [Coverage is checked, so no such `case`
153    /// compiles](../../../docs/spec/evaluation-semantics.md#two-outcomes) — but nothing
154    /// checks it yet (`LANG-19`), so a `case` missing a branch compiles today and
155    /// reaches this leaf at runtime. It is not deleted the day `LANG-19` lands: an
156    /// unreachable leaf still has to
157    /// [abort](../../../docs/spec/evaluation-semantics.md#when-a-program-aborts) rather
158    /// than fall through to whatever follows it, and this is that leaf — merely one
159    /// exhaustiveness will have made unreachable, rather than one still worth reaching
160    /// for.
161    Fail {
162        /// The declaration the `case` was written in: what the description `$abort`
163        /// takes (`runtime/js/zelkova.mjs`, added by `GEN-8`) is built from.
164        ///
165        /// This is whatever [`build`]'s caller passed, not something the tree finds:
166        /// nothing in a `case`'s branches says which declaration they sit in, so naming
167        /// it is the caller's contract — a backend walking a declaration's body has
168        /// that declaration's name at hand. Neither the `$abort` call nor the
169        /// description's exact wording is this pass's to write; walking a `Decision`
170        /// to build them is `zelkova_js::emit`'s.
171        declaration: Name,
172    },
173}
174
175impl PartialEq for Decision<'_> {
176    fn eq(&self, other: &Self) -> bool {
177        match (self, other) {
178            (
179                Decision::Test {
180                    scrutinee,
181                    outcome,
182                    matched,
183                    default,
184                },
185                Decision::Test {
186                    scrutinee: other_scrutinee,
187                    outcome: other_outcome,
188                    matched: other_matched,
189                    default: other_default,
190                },
191            ) => {
192                scrutinee == other_scrutinee
193                    && outcome == other_outcome
194                    && matched == other_matched
195                    && default == other_default
196            }
197            (
198                Decision::Leaf { bindings, body },
199                Decision::Leaf {
200                    bindings: other_bindings,
201                    body: other_body,
202                },
203            ) => bindings == other_bindings && std::ptr::eq(*body, *other_body),
204            (
205                Decision::Fail { declaration },
206                Decision::Fail {
207                    declaration: other_declaration,
208                },
209            ) => declaration == other_declaration,
210            _ => false,
211        }
212    }
213}
214
215/// Lower one `case`'s branches, in source order, into the [`Decision`] a backend walks.
216///
217/// `scrutinee` is the type of the value the `case` matches on — the solved type of
218/// [`super::TypedTermKind::Case`]'s own `scrutinee` — which a variable written as a
219/// whole branch's pattern binds. `branches` is that `case`'s own, in the same order. And
220/// `declaration` names the value the `case` was written inside of, which is what a
221/// [`Decision::Fail`] leaf, if the tree needs one, carries (see that field's doc).
222pub fn build<'a>(
223    scrutinee: &Type,
224    branches: &'a [(TermPattern, Box<TypedTerm>)],
225    declaration: &Name,
226) -> Decision<'a> {
227    let Some(((pattern, body), rest)) = branches.split_first() else {
228        return Decision::Fail {
229            declaration: declaration.clone(),
230        };
231    };
232
233    let on_fail = build(scrutinee, rest, declaration);
234    lower(
235        vec![(Occurrence::Root, scrutinee.clone(), pattern)],
236        Vec::new(),
237        body,
238        &on_fail,
239    )
240}
241
242/// One branch's pattern, still to be lowered: the sub-patterns not yet walked, each at
243/// its occurrence and with the type of the value there, as a stack whose next entry is
244/// its last; the bindings gathered so far; the branch's body; and the tree for the
245/// branches after it, which every `Test` falls back to.
246fn lower<'a>(
247    mut pending: Vec<(Occurrence, Type, &'a TermPattern)>,
248    mut bindings: Vec<Binding>,
249    body: &'a TypedTerm,
250    on_fail: &Decision<'a>,
251) -> Decision<'a> {
252    let Some((occurrence, tpe, pattern)) = pending.pop() else {
253        return Decision::Leaf { bindings, body };
254    };
255
256    match &pattern.kind {
257        // The unit type has one value, so a value of it always matches `()`: no test,
258        // and nothing to bind. A constructor that did not resolve has no case to test
259        // for; no backend emits a tree built from one, since an error stands behind it.
260        TermPatternKind::Anything | TermPatternKind::Unit | TermPatternKind::Hole { .. } => {
261            lower(pending, bindings, body, on_fail)
262        }
263        TermPatternKind::Bind(name) => {
264            bindings.push(Binding {
265                name: name.clone(),
266                occurrence,
267                tpe,
268            });
269            lower(pending, bindings, body, on_fail)
270        }
271        TermPatternKind::Tuple { elements } => {
272            let elements: Vec<_> = elements.iter().enumerate().collect();
273            for (position, element) in elements.into_iter().rev() {
274                pending.push((
275                    occurrence.field(Step::TupleElement(position)),
276                    element.tpe.clone(),
277                    &element.pattern,
278                ));
279            }
280            lower(pending, bindings, body, on_fail)
281        }
282        // Reversed for the reason a tuple's elements are: the next entry walked is the
283        // first written.
284        TermPatternKind::Record { fields } => {
285            for field in fields.iter().rev() {
286                pending.push((
287                    occurrence.field(Step::Field(field.label.clone())),
288                    field.value.tpe.clone(),
289                    &field.value.pattern,
290                ));
291            }
292            lower(pending, bindings, body, on_fail)
293        }
294        TermPatternKind::Literal { value, .. } => Decision::Test {
295            matched: Box::new(lower(pending, bindings, body, on_fail)),
296            default: Box::new(on_fail.clone()),
297            outcome: Outcome::Literal(value.clone()),
298            scrutinee: occurrence,
299        },
300        TermPatternKind::Constructor { ctor, args, .. } => {
301            for (position, arg) in args.iter().enumerate().rev() {
302                pending.push((
303                    occurrence.field(Step::ConstructorArgument(position)),
304                    arg.tpe.clone(),
305                    &arg.pattern,
306                ));
307            }
308            Decision::Test {
309                matched: Box::new(lower(pending, bindings, body, on_fail)),
310                default: Box::new(on_fail.clone()),
311                outcome: Outcome::Constructor(ctor.clone()),
312                scrutinee: occurrence,
313            }
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    //! These build each [`TermPattern`] by hand, the shape `typer::translate_pattern`
321    //! produces, and pin what [`build`] makes of it with no typer in the way.
322    //! `crates/zelkova-compiler/tests/ir.rs` builds the same kind of tree from source.
323
324    use super::*;
325    use crate::ir::{SubPattern, TypedTermKind};
326    use crate::name::QualName;
327    use crate::typer::TypeLiteral;
328    use crate::PackageName;
329    use zelkova_syntax::position::NodeSpan;
330    use zelkova_syntax::tuple::Tuple;
331
332    fn int() -> Type {
333        Type::Literal(TypeLiteral::Int)
334    }
335
336    fn maybe(of: Type) -> Type {
337        Type::Adt(
338            QualName::in_module(PackageName::core(), "Maybe", "Maybe"),
339            vec![of],
340        )
341    }
342
343    fn pattern(kind: TermPatternKind) -> TermPattern {
344        TermPattern {
345            span: NodeSpan::none(),
346            kind,
347        }
348    }
349
350    fn sub(tpe: Type, kind: TermPatternKind) -> SubPattern {
351        SubPattern {
352            tpe,
353            pattern: pattern(kind),
354        }
355    }
356
357    fn just() -> Constructor {
358        Constructor {
359            union: QualName::in_module(PackageName::core(), "Maybe", "Maybe"),
360            name: Name::new("Just"),
361            index: 0,
362            arity: 1,
363        }
364    }
365
366    fn just_pattern(of: Type, arg: SubPattern) -> TermPatternKind {
367        TermPatternKind::Constructor {
368            ctor: just(),
369            adt_args: vec![of],
370            args: vec![arg],
371        }
372    }
373
374    fn body(value: i64) -> Box<TypedTerm> {
375        Box::new(TypedTerm {
376            span: NodeSpan::none(),
377            tpe: int(),
378            kind: TypedTermKind::Int(value),
379        })
380    }
381
382    /// `Just (Just x) -> 1 ; _ -> 0` on a `Maybe (Maybe Int)`: the inner `Just` is a
383    /// `Test` of its own, one step below the root, whose `default` is the wildcard
384    /// branch's leaf — the same tree the outer `Test` falls back to — and `x` is bound
385    /// two steps down, at the inner argument's type.
386    ///
387    /// Mutation-checked by having `lower`'s `Constructor` arm bind each argument's
388    /// sub-pattern at `occurrence` rather than at `occurrence.field(..)`: the inner
389    /// `Test`'s scrutinee comes out as the root, and the assertion goes red.
390    #[test]
391    fn a_nested_constructor_is_a_test_below_the_root() {
392        let inner = sub(
393            maybe(int()),
394            just_pattern(int(), sub(int(), TermPatternKind::Bind("x".to_string()))),
395        );
396        let branches = vec![
397            (pattern(just_pattern(maybe(int()), inner)), body(1)),
398            (pattern(TermPatternKind::Anything), body(0)),
399        ];
400
401        let tree = build(&maybe(maybe(int())), &branches, &Name::new("f"));
402
403        let fallback = Decision::Leaf {
404            bindings: vec![],
405            body: &branches[1].1,
406        };
407        let argument = Occurrence::Root.field(Step::ConstructorArgument(0));
408        assert_eq!(
409            tree,
410            Decision::Test {
411                scrutinee: Occurrence::Root,
412                outcome: Outcome::Constructor(just()),
413                matched: Box::new(Decision::Test {
414                    scrutinee: argument.clone(),
415                    outcome: Outcome::Constructor(just()),
416                    matched: Box::new(Decision::Leaf {
417                        bindings: vec![Binding {
418                            name: "x".to_string(),
419                            occurrence: argument.field(Step::ConstructorArgument(0)),
420                            tpe: int(),
421                        }],
422                        body: &branches[0].1,
423                    }),
424                    default: Box::new(fallback.clone()),
425                }),
426                default: Box::new(fallback),
427            }
428        );
429    }
430
431    /// `(1, y, 2) -> 1 ; _ -> 0` on an `(Int, Int, Int)`: the tuple itself is not
432    /// tested, its first and third elements are, in that order, and `y` is bound only
433    /// once both tests have passed. Each of the two `Test`s falls back to the wildcard
434    /// branch's leaf, its own copy of it.
435    ///
436    /// Mutation-checked by pushing a tuple's elements onto `lower`'s stack in source
437    /// order rather than reversed, so they are walked right to left: the third
438    /// element's `Test` then comes first, and the assertion goes red.
439    #[test]
440    fn literals_inside_a_tuple_are_tested_at_their_elements_left_to_right() {
441        let literal = |value| {
442            sub(
443                int(),
444                TermPatternKind::Literal {
445                    tpe: int(),
446                    value: LiteralValue::Int(value),
447                },
448            )
449        };
450        let tuple = TermPatternKind::Tuple {
451            elements: Tuple::three(
452                literal(1),
453                sub(int(), TermPatternKind::Bind("y".to_string())),
454                literal(2),
455            ),
456        };
457        let branches = vec![
458            (pattern(tuple), body(1)),
459            (pattern(TermPatternKind::Anything), body(0)),
460        ];
461
462        let tree = build(
463            &Type::Tuple(Tuple::three(int(), int(), int())),
464            &branches,
465            &Name::new("f"),
466        );
467
468        let fallback = Decision::Leaf {
469            bindings: vec![],
470            body: &branches[1].1,
471        };
472        let element = |position| Occurrence::Root.field(Step::TupleElement(position));
473        assert_eq!(
474            tree,
475            Decision::Test {
476                scrutinee: element(0),
477                outcome: Outcome::Literal(LiteralValue::Int(1)),
478                matched: Box::new(Decision::Test {
479                    scrutinee: element(2),
480                    outcome: Outcome::Literal(LiteralValue::Int(2)),
481                    matched: Box::new(Decision::Leaf {
482                        bindings: vec![Binding {
483                            name: "y".to_string(),
484                            occurrence: element(1),
485                            tpe: int(),
486                        }],
487                        body: &branches[0].1,
488                    }),
489                    default: Box::new(fallback.clone()),
490                }),
491                default: Box::new(fallback),
492            }
493        );
494    }
495}