diff --git a/crates/compiler/can/src/expr.rs b/crates/compiler/can/src/expr.rs index 6146d472e6..42e5633894 100644 --- a/crates/compiler/can/src/expr.rs +++ b/crates/compiler/can/src/expr.rs @@ -1270,6 +1270,59 @@ fn canonicalize_closure_body<'a>( (closure_data, output) } +enum MultiPatternVariables { + OnePattern, + MultiPattern { + bound_occurences: VecMap, + }, +} + +impl MultiPatternVariables { + #[inline(always)] + fn new(num_patterns: usize) -> Self { + if num_patterns > 1 { + Self::MultiPattern { + bound_occurences: VecMap::with_capacity(2), + } + } else { + Self::OnePattern + } + } + + #[inline(always)] + fn add_pattern(&mut self, pattern: &Loc) { + match self { + MultiPatternVariables::OnePattern => {} + MultiPatternVariables::MultiPattern { bound_occurences } => { + for (sym, region) in BindingsFromPattern::new(pattern) { + if !bound_occurences.contains_key(&sym) { + bound_occurences.insert(sym, (region, 0)); + } + bound_occurences.get_mut(&sym).unwrap().1 += 1; + } + } + } + } + + #[inline(always)] + fn get_unbound(self) -> impl Iterator { + let bound_occurences = match self { + MultiPatternVariables::OnePattern => Default::default(), + MultiPatternVariables::MultiPattern { bound_occurences } => bound_occurences, + }; + + bound_occurences + .into_iter() + .filter_map(|(sym, (region, occurs))| { + if occurs == 1 { + Some((sym, region)) + } else { + None + } + }) + } +} + #[inline(always)] fn canonicalize_when_branch<'a>( env: &mut Env<'a>, @@ -1280,6 +1333,7 @@ fn canonicalize_when_branch<'a>( output: &mut Output, ) -> (WhenBranch, References) { let mut patterns = Vec::with_capacity(branch.patterns.len()); + let mut multi_pattern_variables = MultiPatternVariables::new(branch.patterns.len()); // TODO report symbols not bound in all patterns for loc_pattern in branch.patterns.iter() { @@ -1294,9 +1348,17 @@ fn canonicalize_when_branch<'a>( PermitShadows(true), ); + multi_pattern_variables.add_pattern(&can_pattern); patterns.push(can_pattern); } + for (unbound_symbol, region) in multi_pattern_variables.get_unbound() { + env.problem(Problem::NotBoundInAllPatterns { + unbound_symbol, + region, + }) + } + let (value, mut branch_output) = canonicalize_expr( env, var_store, diff --git a/crates/compiler/constrain/src/expr.rs b/crates/compiler/constrain/src/expr.rs index b5a9b3d547..7d350ee5bd 100644 --- a/crates/compiler/constrain/src/expr.rs +++ b/crates/compiler/constrain/src/expr.rs @@ -1843,24 +1843,28 @@ fn constrain_when_branch_help( if i == 0 { state.headers.extend(partial_state.headers); } else { - debug_assert!( - state.headers.keys().all(|sym| partial_state.headers.contains_key(sym)) && - partial_state.headers.keys().all(|sym| state.headers.contains_key(sym)), - "State and partial state headers differ in bound symbols, should have been caught in canonicalization"); - // Make sure the bound variables in the patterns on the same branch agree in their types. for (sym, typ1) in state.headers.iter() { - let typ2 = partial_state - .headers - .get(sym) - .expect("bound variable in branch not bound in pattern!"); + if let Some(typ2) = partial_state.headers.get(sym) { + state.constraints.push(constraints.equal_types( + typ1.value.clone(), + Expected::NoExpectation(typ2.value.clone()), + Category::When, + typ2.region, + )); + } - state.constraints.push(constraints.equal_types( - typ1.value.clone(), - Expected::NoExpectation(typ2.value.clone()), - Category::When, - typ2.region, - )); + // If the pattern doesn't bind all symbols introduced in the branch we'll have + // reported a canonicalization error, but still might reach here; that's okay. + } + + // Add any variables this pattern binds that the other patterns don't bind. + // This will already have been reported as an error, but we still might be able to + // solve their types. + for (sym, ty) in partial_state.headers { + if !state.headers.contains_key(&sym) { + state.headers.insert(sym, ty); + } } } } diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index e52e6a7289..786dca0a4f 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -165,6 +165,10 @@ pub enum Problem { ability: Symbol, not_implemented: Vec, }, + NotBoundInAllPatterns { + unbound_symbol: Symbol, + region: Region, + }, } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/compiler/solve/tests/solve_expr.rs b/crates/compiler/solve/tests/solve_expr.rs index 6323b0623a..d249900f1f 100644 --- a/crates/compiler/solve/tests/solve_expr.rs +++ b/crates/compiler/solve/tests/solve_expr.rs @@ -7370,11 +7370,11 @@ mod solve_expr { "# ), @r###" - A "" : [A Str, B Str] - x : Str - x : Str - x : Str - "### + A "" : [A Str, B Str] + x : Str + x : Str + x : Str + "### ); } } diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index cd9c9afba5..fe176ed976 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -902,6 +902,27 @@ pub fn can_problem<'b>( title = INCOMPLETE_ABILITY_IMPLEMENTATION.to_string(); severity = Severity::RuntimeError; } + Problem::NotBoundInAllPatterns { + unbound_symbol, + region, + } => { + doc = alloc.stack([ + alloc.concat([ + alloc.symbol_unqualified(unbound_symbol), + alloc.reflow(" is not bound in all patterns of this "), + alloc.keyword("when"), + alloc.reflow(" branch"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Identifiers introduced in a "), + alloc.keyword("when"), + alloc.reflow(" branch must be bound in all patterns of the branch. Otherwise, the program would crash when it tries to use an identifier that wasn't bound!"), + ]), + ]); + title = "NAME NOT BOUND IN ALL PATTERNS".to_string(); + severity = Severity::RuntimeError; + } }; Report { diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index 33921f969e..77084a0700 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -9832,4 +9832,47 @@ All branches in an `if` must have the same type! U16 -> A "### ); + + test_report!( + symbols_not_bound_in_all_patterns, + indoc!( + r#" + when A "" is + A x | B y -> x + "# + ), + @r###" + ── NAME NOT BOUND IN ALL PATTERNS ──────────────────────── /code/proj/Main.roc ─ + + `x` is not bound in all patterns of this `when` branch + + 5│ A x | B y -> x + ^ + + Identifiers introduced in a `when` branch must be bound in all patterns + of the branch. Otherwise, the program would crash when it tries to use + an identifier that wasn't bound! + + ── NAME NOT BOUND IN ALL PATTERNS ──────────────────────── /code/proj/Main.roc ─ + + `y` is not bound in all patterns of this `when` branch + + 5│ A x | B y -> x + ^ + + Identifiers introduced in a `when` branch must be bound in all patterns + of the branch. Otherwise, the program would crash when it tries to use + an identifier that wasn't bound! + + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + + `y` is not used anywhere in your code. + + 5│ A x | B y -> x + ^ + + If you didn't intend on using `y` then remove it so future readers of + your code don't wonder why it is there. + "### + ); }