diff --git a/compiler/can/src/abilities.rs b/compiler/can/src/abilities.rs index 16c90273e2..453cded77f 100644 --- a/compiler/can/src/abilities.rs +++ b/compiler/can/src/abilities.rs @@ -1,4 +1,4 @@ -use roc_collections::all::{MutMap, MutSet}; +use roc_collections::all::MutMap; use roc_module::symbol::Symbol; use roc_region::all::Region; use roc_types::{subs::Variable, types::Type}; @@ -13,6 +13,13 @@ pub struct AbilityMemberData { pub region: Region, } +/// A particular specialization of an ability member. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MemberSpecialization { + pub symbol: Symbol, + pub region: Region, +} + /// Stores information about what abilities exist in a scope, what it means to implement an /// ability, and what types implement them. // TODO(abilities): this should probably go on the Scope, I don't put it there for now because we @@ -35,9 +42,9 @@ pub struct AbilitiesStore { /// We keep the mapping #hash1->#hash specialization_to_root: MutMap, - /// Tuples of (type, member) specifying that `type` declares an implementation of an ability - /// member `member`. - declared_specializations: MutSet<(Symbol, Symbol)>, + /// Maps a tuple (member, type) specifying that `type` declares an implementation of an ability + /// member `member`, to the exact symbol that implements the ability. + declared_specializations: MutMap<(Symbol, Symbol), MemberSpecialization>, } impl AbilitiesStore { @@ -69,15 +76,18 @@ impl AbilitiesStore { } /// Records a specialization of `ability_member` with specialized type `implementing_type`. + /// Entries via this function are considered a source of truth. It must be ensured that a + /// specialization is validated before being registered here. pub fn register_specialization_for_type( &mut self, - implementing_type: Symbol, ability_member: Symbol, + implementing_type: Symbol, + specialization: MemberSpecialization, ) { - let is_new_insert = self + let old_spec = self .declared_specializations - .insert((implementing_type, ability_member)); - debug_assert!(is_new_insert, "Replacing existing implementation"); + .insert((ability_member, implementing_type), specialization); + debug_assert!(old_spec.is_none(), "Replacing existing specialization"); } /// Checks if `name` is a root ability member symbol name. @@ -123,9 +133,27 @@ impl AbilitiesStore { Some((*root_symbol, root_data)) } + /// Finds the ability member definition for a member name. + pub fn member_def(&self, member: Symbol) -> Option<&AbilityMemberData> { + self.ability_members.get(&member) + } + + /// Returns an iterator over pairs (ability member, type) specifying that + /// "ability member" has a specialization with type "type". + pub fn get_known_specializations<'lifetime_for_copied>( + &'lifetime_for_copied self, + ) -> impl Iterator + 'lifetime_for_copied { + self.declared_specializations.keys().copied() + } + + /// Retrieves the specialization of `member` for `typ`, if it exists. + pub fn get_specialization(&self, member: Symbol, typ: Symbol) -> Option { + self.declared_specializations.get(&(member, typ)).copied() + } + /// Returns pairs of (type, ability member) specifying that "ability member" has a /// specialization with type "type". - pub fn get_known_specializations(&self) -> &MutSet<(Symbol, Symbol)> { - &self.declared_specializations + pub fn members_of_ability(&self, ability: Symbol) -> Option<&[Symbol]> { + self.members_of_ability.get(&ability).map(|v| v.as_ref()) } } diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs index b0042086c4..b5e83160f9 100644 --- a/compiler/solve/src/solve.rs +++ b/compiler/solve/src/solve.rs @@ -1,5 +1,5 @@ use bumpalo::Bump; -use roc_can::abilities::AbilitiesStore; +use roc_can::abilities::{AbilitiesStore, MemberSpecialization}; use roc_can::constraint::Constraint::{self, *}; use roc_can::constraint::{Constraints, LetConstraint}; use roc_can::expected::{Expected, PExpected}; @@ -76,6 +76,13 @@ pub enum TypeError { CircularType(Region, Symbol, ErrorType), BadType(roc_types::types::Problem), UnexposedLookup(Symbol), + IncompleteAbilityImplementation { + // TODO(abilities): have general types here, not just opaques + typ: Symbol, + ability: Symbol, + specialized_members: Vec>, + missing_members: Vec>, + }, } use roc_types::types::Alias; @@ -568,7 +575,34 @@ pub fn run_in_place( &mut deferred_must_implement_abilities, ); - // TODO run through and check that all abilities are properly implemented + // Now that the module has been solved, we can run through and check all + // types claimed to implement abilities. + deferred_must_implement_abilities.dedup(); + for MustImplementAbility { typ, ability } in deferred_must_implement_abilities.into_iter() { + let members_of_ability = abilities_store.members_of_ability(ability).unwrap(); + let mut specialized_members = Vec::with_capacity(members_of_ability.len()); + let mut missing_members = Vec::with_capacity(members_of_ability.len()); + for &member in members_of_ability { + match abilities_store.get_specialization(member, typ) { + None => { + let root_data = abilities_store.member_def(member).unwrap(); + missing_members.push(Loc::at(root_data.region, member)); + } + Some(specialization) => { + specialized_members.push(Loc::at(specialization.region, member)); + } + } + } + + if !missing_members.is_empty() { + problems.push(TypeError::IncompleteAbilityImplementation { + typ, + ability, + specialized_members, + missing_members, + }); + } + } state.env } @@ -1254,8 +1288,17 @@ fn check_ability_specialization( "If there's more than one, the definition is ambiguous - this should be an error" ); + // This is a valid specialization! Record it. let specialization_type = ability_implementations_for_specialization[0].typ; - abilities_store.register_specialization_for_type(specialization_type, root_symbol); + let specialization = MemberSpecialization { + symbol, + region: symbol_loc_var.region, + }; + abilities_store.register_specialization_for_type( + root_symbol, + specialization_type, + specialization, + ); // Store the checks for what abilities must be implemented to be checked after the // whole module is complete. diff --git a/reporting/src/error/type.rs b/reporting/src/error/type.rs index 737ea2be40..c98d125e3d 100644 --- a/reporting/src/error/type.rs +++ b/reporting/src/error/type.rs @@ -118,6 +118,53 @@ pub fn type_problem<'b>( other => panic!("unhandled bad type: {:?}", other), } } + IncompleteAbilityImplementation { + typ, + ability, + specialized_members, + missing_members, + } => { + let title = "INCOMPLETE ABILITY IMPLEMENTATION".to_string(); + + let mut stack = vec![alloc.concat(vec![ + alloc.reflow("The type "), + alloc.symbol_unqualified(typ), + alloc.reflow(" does not fully implement the ability "), + alloc.symbol_unqualified(ability), + alloc.reflow(". The following specializations are missing:"), + ])]; + + for member in missing_members.into_iter() { + stack.push(alloc.concat(vec![ + alloc.reflow("A specialization for "), + alloc.symbol_unqualified(member.value), + alloc.reflow(", which is defined here:"), + ])); + stack.push(alloc.region(lines.convert_region(member.region))); + } + + debug_assert!(!specialized_members.is_empty()); + + stack.push(alloc.concat(vec![ + alloc.note(""), + alloc.symbol_unqualified(typ), + alloc.reflow(" specializes the following members of "), + alloc.symbol_unqualified(ability), + alloc.reflow(":"), + ])); + + for spec in specialized_members { + stack.push(alloc.concat(vec![ + alloc.symbol_unqualified(spec.value), + alloc.reflow(", specialized here:"), + ])); + stack.push(alloc.region(lines.convert_region(spec.region))); + } + + let doc = alloc.stack(stack); + + report(title, doc, filename) + } } } diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index 8a1fe1d0b9..2b285c334f 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -9462,4 +9462,43 @@ I need all branches in an `if` to have the same type! ), ) } + + #[test] + fn ability_specialization_is_incomplete() { + new_report_problem_as( + indoc!( + r#" + app "test" provides [ eq, le ] to "./platform" + + Eq has + eq : a, a -> Bool | a has Eq + le : a, a -> Bool | a has Eq + + Id := U64 + + eq = \$Id m, $Id n -> m == n + "# + ), + indoc!( + r#" + ── INCOMPLETE ABILITY IMPLEMENTATION ─────────────────────────────────────────── + + The type `Id` does not fully implement the ability `Eq`. The following + specializations are missing: + + A specialization for `le`, which is defined here: + + 5│ le : a, a -> Bool | a has Eq + ^^ + + Note: `Id` specializes the following members of `Eq`: + + `eq`, specialized here: + + 9│ eq = \$Id m, $Id n -> m == n + ^^ + "# + ), + ) + } }