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}