Skip to content

Commit c94f228

Browse files
abs0lutylpil
authored andcommitted
Fix #4278
1 parent 7dad024 commit c94f228

6 files changed

+101
-13
lines changed

compiler-core/src/exhaustiveness.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,20 @@ enum BranchMode {
787787
}
788788

789789
impl BranchMode {
790+
/// Returns a heuristic estimate of the branching factor.
791+
///
792+
/// This value is used by the pivot-selection to prefer splits
793+
/// with fewer branches, which tends to produce smaller and shallower
794+
/// decision trees.
795+
fn branching_factor(&self) -> usize {
796+
match self {
797+
BranchMode::Infinite => usize::MAX,
798+
BranchMode::Tuple { elements } => elements.len(),
799+
BranchMode::List { .. } => 2,
800+
BranchMode::NamedType { constructors, .. } => constructors.len(),
801+
}
802+
}
803+
790804
fn is_infinite(&self) -> bool {
791805
match self {
792806
BranchMode::Infinite => true,
@@ -2250,7 +2264,7 @@ impl<'a> Compiler<'a> {
22502264
// the first branch, and use the variable it's pattern matching on to create
22512265
// a new node in the tree. All the branches will be split into different
22522266
// possible paths of this tree.
2253-
match find_pivot_check(first_branch, &branches) {
2267+
match find_pivot_check(first_branch, &branches, self.environment) {
22542268
Some(PatternCheck { var, .. }) => self.split_and_compile_with_pivot_var(var, branches),
22552269

22562270
// If the branch has no remaining checks, it means that we've moved all
@@ -2713,7 +2727,11 @@ impl<'a> Compiler<'a> {
27132727
/// Returns a pattern check from `first_branch` to be used as a pivot to split all
27142728
/// the `branches`.
27152729
///
2716-
fn find_pivot_check(first_branch: &Branch, branches: &VecDeque<Branch>) -> Option<PatternCheck> {
2730+
fn find_pivot_check(
2731+
first_branch: &Branch,
2732+
branches: &VecDeque<Branch>,
2733+
env: &Environment<'_>,
2734+
) -> Option<PatternCheck> {
27172735
// To try and minimise code duplication, we use the following heuristic: we
27182736
// choose the check matching on the variable that is referenced the most
27192737
// across all checks in all branches.
@@ -2727,10 +2745,28 @@ fn find_pivot_check(first_branch: &Branch, branches: &VecDeque<Branch>) -> Optio
27272745
}
27282746
}
27292747

2748+
// Maximize ( -branching_factor, references ).
2749+
//
2750+
// - branching_factor: prefer the split that creates the FEWEST switch arms.
2751+
// Fewer children tends to yield smaller, shallower decision trees.
2752+
//
2753+
// - references: break ties by choosing the subject that appears in the most
2754+
// clauses (i.e. a test that will pay off for more rows).
2755+
//
2756+
// Both parts mirror standard guidance for good match trees (small branching
2757+
// factor; prioritize columns that are widely useful).
2758+
//
2759+
// https://www.cs.tufts.edu/~nr/cs257/archive/luc-maranget/jun08.pdf
27302760
first_branch
27312761
.checks
27322762
.iter()
2733-
.max_by_key(|check| var_references.get(&check.var.id).cloned().unwrap_or(0))
2763+
.max_by_key(|check| {
2764+
let mode = check.var.branch_mode(env);
2765+
let branching_factor = mode.branching_factor();
2766+
let references = var_references.get(&check.var.id).cloned().unwrap_or(0);
2767+
2768+
(usize::MAX - branching_factor, references)
2769+
})
27342770
.cloned()
27352771
}
27362772

compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__assignments__nested_binding.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export function go(x) {
1919
let t;
2020
let b;
2121
let c;
22-
let $ = x[3];
23-
if ($ === 1) {
24-
let $1 = x[1][2];
25-
if ($1 === 2) {
22+
let $ = x[1][2];
23+
if ($ === 2) {
24+
let $1 = x[3];
25+
if ($1 === 1) {
2626
a = x[0];
2727
t = x[1];
2828
b = x[1][0];

compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_multiple_subjects_building_same_value_as_two_subjects_one_is_picked.snap

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ import { Ok, Error } from "../gleam.mjs";
2020

2121
export function go(x, y) {
2222
if (y instanceof Ok) {
23-
let $ = y[0];
24-
if ($ === 1 && x instanceof $gleam.Ok) {
25-
let $1 = x[0];
26-
if ($1 === 1) {
27-
return x;
23+
if (x instanceof $gleam.Ok) {
24+
let $ = x[0];
25+
if ($ === 1) {
26+
let $1 = y[0];
27+
if ($1 === 1) {
28+
return x;
29+
} else {
30+
return new Error(undefined);
31+
}
2832
} else {
2933
return new Error(undefined);
3034
}

compiler-core/src/type_/tests/exhaustiveness.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,3 +1787,19 @@ pub fn main(w: Wibble) {
17871787
"
17881788
);
17891789
}
1790+
1791+
#[test]
1792+
// https://github.com/gleam-lang/gleam/issues/4278
1793+
fn redundant_missing_patterns() {
1794+
assert_module_error!(
1795+
r#"
1796+
fn wibble(b: Bool, i: Int) {
1797+
case b, i {
1798+
False, 1 -> todo
1799+
True, 2 -> todo
1800+
}
1801+
}
1802+
1803+
pub fn main() { wibble(False, 1) }"#
1804+
);
1805+
}

compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__nested_type_parameter_usage.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ one of the other values is used then the assignment will crash.
2626

2727
The missing patterns are:
2828

29+
Returned([#(), _, ..])
2930
Returned([])
30-
Returned([_, _, ..])
3131

3232
Hint: Use a more general pattern or use `let assert` instead.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
source: compiler-core/src/type_/tests/exhaustiveness.rs
3+
expression: "\nfn wibble(b: Bool, i: Int) {\n case b, i {\n False, 1 -> todo\n True, 2 -> todo\n }\n}\n\npub fn main() { wibble(False, 1) }"
4+
---
5+
----- SOURCE CODE
6+
7+
fn wibble(b: Bool, i: Int) {
8+
case b, i {
9+
False, 1 -> todo
10+
True, 2 -> todo
11+
}
12+
}
13+
14+
pub fn main() { wibble(False, 1) }
15+
16+
----- ERROR
17+
error: Inexhaustive patterns
18+
┌─ /src/one/two.gleam:3:3
19+
20+
3 │ ╭ case b, i {
21+
4 │ │ False, 1 -> todo
22+
5 │ │ True, 2 -> todo
23+
6 │ │ }
24+
│ ╰───^
25+
26+
This case expression does not have a pattern for all possible values. If it
27+
is run on one of the values without a pattern then it will crash.
28+
29+
The missing patterns are:
30+
31+
False, _
32+
True, _

0 commit comments

Comments
 (0)