diff --git a/crates/compiler/can/src/annotation.rs b/crates/compiler/can/src/annotation.rs index d977ce14bc..c3fc13e13a 100644 --- a/crates/compiler/can/src/annotation.rs +++ b/crates/compiler/can/src/annotation.rs @@ -931,31 +931,37 @@ fn canonicalize_has_clause( let var_name = Lowercase::from(var_name); let mut can_abilities = Vec::with_capacity(abilities.len()); - for ability in *abilities { - let ability = match ability.value { + for &Loc { + region, + value: ability, + } in *abilities + { + let ability = match ability { TypeAnnotation::Apply(module_name, ident, _type_arguments) => { - let symbol = make_apply_symbol(env, ability.region, scope, module_name, ident)?; + let symbol = make_apply_symbol(env, region, scope, module_name, ident)?; // Ability defined locally, whose members we are constructing right now... if !pending_abilities_in_scope.contains_key(&symbol) // or an ability that was imported from elsewhere && !scope.abilities_store.is_ability(symbol) { - let region = ability.region; env.problem(roc_problem::can::Problem::HasClauseIsNotAbility { region }); return Err(Type::Erroneous(Problem::HasClauseIsNotAbility(region))); } symbol } _ => { - let region = ability.region; env.problem(roc_problem::can::Problem::HasClauseIsNotAbility { region }); return Err(Type::Erroneous(Problem::HasClauseIsNotAbility(region))); } }; references.insert(ability); - can_abilities.push(ability); + if can_abilities.contains(&ability) { + env.problem(roc_problem::can::Problem::DuplicateHasAbility { ability, region }); + } else { + can_abilities.push(ability); + } } if let Some(shadowing) = introduced_variables.named_var_by_name(&var_name) { diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index a59ebd1093..ca0d50fd4c 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -117,6 +117,10 @@ pub enum Problem { IllegalHasClause { region: Region, }, + DuplicateHasAbility { + ability: Symbol, + region: Region, + }, AbilityMemberMissingHasClause { member: Symbol, ability: Symbol, diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index d9bc25db44..361d582261 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -677,6 +677,24 @@ pub fn can_problem<'b>( severity = Severity::RuntimeError; } + Problem::DuplicateHasAbility { ability, region } => { + doc = alloc.stack([ + alloc.concat([ + alloc.reflow("I already saw that this type variable is bound to the "), + alloc.symbol_foreign_qualified(ability), + alloc.reflow(" ability once before:"), + ]), + alloc.region(lines.convert_region(region)), + alloc.concat([ + alloc.reflow("Abilities only need to bound to a type variable once in a "), + alloc.keyword("has"), + alloc.reflow(" clause!"), + ]), + ]); + title = "DUPLICATE BOUND ABILITY".to_string(); + severity = Severity::Warning; + } + Problem::AbilityMemberMissingHasClause { member, ability, diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index 6a6239cf50..1d47fd3cfa 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -11531,4 +11531,26 @@ All branches in an `if` must have the same type! Tip: You can define a custom implementation of `Encoding` for `F`. "### ); + + test_report!( + duplicate_ability_in_has_clause, + indoc!( + r#" + f : a -> {} | a has Hash & Hash + + f + "# + ), + @r###" + ── DUPLICATE BOUND ABILITY ─────────────────────────────── /code/proj/Main.roc ─ + + I already saw that this type variable is bound to the `Hash` ability + once before: + + 4│ f : a -> {} | a has Hash & Hash + ^^^^ + + Abilities only need to bound to a type variable once in a `has` clause! + "### + ); }