Report incomplete ability implementations

This commit is contained in:
Ayaz Hafiz 2022-04-12 18:24:10 -04:00
parent b86bf94d92
commit 127d435ff2
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
4 changed files with 170 additions and 13 deletions

View file

@ -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<Symbol, Symbol>,
/// 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<Item = (Symbol, Symbol)> + '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<MemberSpecialization> {
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())
}
}

View file

@ -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<Loc<Symbol>>,
missing_members: Vec<Loc<Symbol>>,
},
}
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.

View file

@ -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)
}
}
}

View file

@ -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
^^
"#
),
)
}
}