Basic type inference and solving for abilities

Note that is still pretty limited. We only permit opaque types to
implement abilities, abilities cannot have type arguments, and also no
other functions may depend on abilities
This commit is contained in:
Ayaz Hafiz 2022-04-12 15:37:16 -04:00
parent 913d97cab1
commit 15a040ec87
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
25 changed files with 1808 additions and 477 deletions

View file

@ -1,55 +1,62 @@
use roc_collections::all::{MutMap, MutSet};
use roc_module::symbol::Symbol;
use roc_types::types::Type;
use crate::annotation::HasClause;
use roc_region::all::Region;
use roc_types::{subs::Variable, types::Type};
/// Stores information about an ability member definition, including the parent ability, the
/// defining type, and what type variables need to be instantiated with instances of the ability.
#[derive(Debug)]
struct AbilityMemberData {
#[allow(unused)]
parent_ability: Symbol,
#[allow(unused)]
signature: Type,
#[allow(unused)]
bound_has_clauses: Vec<HasClause>,
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbilityMemberData {
pub parent_ability: Symbol,
pub signature_var: Variable,
pub signature: Type,
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
// are only dealing with inter-module abilities for now.
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct AbilitiesStore {
/// Maps an ability to the members defining it.
#[allow(unused)]
members_of_ability: MutMap<Symbol, Vec<Symbol>>,
/// Information about all members composing abilities.
ability_members: MutMap<Symbol, AbilityMemberData>,
/// Map of symbols that specialize an ability member to the root ability symbol name.
/// For example, for the program
/// Hash has hash : a -> U64 | a has Hash
/// ^^^^ gets the symbol "#hash"
/// hash = \@Id n -> n
/// ^^^^ gets the symbol "#hash1"
///
/// 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`.
#[allow(unused)]
declared_implementations: MutSet<(Symbol, Symbol)>,
declared_specializations: MutSet<(Symbol, Symbol)>,
}
impl AbilitiesStore {
/// Records the definition of an ability, including its members.
pub fn register_ability(
&mut self,
ability: Symbol,
members: Vec<(Symbol, Type, Vec<HasClause>)>,
members: Vec<(Symbol, Region, Variable, Type)>,
) {
let mut members_vec = Vec::with_capacity(members.len());
for (member, signature, bound_has_clauses) in members.into_iter() {
for (member, region, signature_var, signature) in members.into_iter() {
members_vec.push(member);
let old_member = self.ability_members.insert(
member,
AbilityMemberData {
parent_ability: ability,
signature_var,
signature,
bound_has_clauses,
region,
},
);
debug_assert!(old_member.is_none(), "Replacing existing member definition");
@ -61,14 +68,64 @@ impl AbilitiesStore {
);
}
pub fn register_implementation(&mut self, implementing_type: Symbol, ability_member: Symbol) {
let old_impl = self
.declared_implementations
/// Records a specialization of `ability_member` with specialized type `implementing_type`.
pub fn register_specialization_for_type(
&mut self,
implementing_type: Symbol,
ability_member: Symbol,
) {
let is_new_insert = self
.declared_specializations
.insert((implementing_type, ability_member));
debug_assert!(!old_impl, "Replacing existing implementation");
debug_assert!(is_new_insert, "Replacing existing implementation");
}
/// Checks if `name` is a root ability member symbol name.
/// Note that this will return `false` for specializations of an ability member, which have
/// different symbols from the root.
pub fn is_ability_member_name(&self, name: Symbol) -> bool {
self.ability_members.contains_key(&name)
}
/// Returns information about all known ability members and their root symbols.
pub fn root_ability_members(&self) -> &MutMap<Symbol, AbilityMemberData> {
&self.ability_members
}
/// Records that the symbol `specializing_symbol` claims to specialize `ability_member`; for
/// example the symbol of `hash : Id -> U64` specializing `hash : a -> U64 | a has Hash`.
pub fn register_specializing_symbol(
&mut self,
specializing_symbol: Symbol,
ability_member: Symbol,
) {
self.specialization_to_root
.insert(specializing_symbol, ability_member);
}
/// Returns whether a symbol is declared to specialize an ability member.
pub fn is_specialization_name(&self, symbol: Symbol) -> bool {
self.specialization_to_root.contains_key(&symbol)
}
/// Finds the symbol name and ability member definition for a symbol specializing the ability
/// member, if it specializes any.
/// For example, suppose `hash : Id -> U64` has symbol #hash1 and specializes
/// `hash : a -> U64 | a has Hash` with symbol #hash. Calling this with #hash1 would retrieve
/// the ability member data for #hash.
pub fn root_name_and_def(
&self,
specializing_symbol: Symbol,
) -> Option<(Symbol, &AbilityMemberData)> {
let root_symbol = self.specialization_to_root.get(&specializing_symbol)?;
debug_assert!(self.ability_members.contains_key(&root_symbol));
let root_data = self.ability_members.get(&root_symbol).unwrap();
Some((*root_symbol, root_data))
}
/// 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
}
}

View file

@ -19,6 +19,22 @@ pub struct Annotation {
pub aliases: SendMap<Symbol, Alias>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum NamedOrAbleVariable<'a> {
Named(&'a NamedVariable),
Able(&'a AbleVariable),
}
impl<'a> NamedOrAbleVariable<'a> {
pub fn first_seen(&self) -> Region {
match self {
NamedOrAbleVariable::Named(nv) => nv.first_seen,
NamedOrAbleVariable::Able(av) => av.first_seen,
}
}
}
/// A named type variable, not bound to an ability.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct NamedVariable {
pub variable: Variable,
@ -27,21 +43,40 @@ pub struct NamedVariable {
pub first_seen: Region,
}
/// A type variable bound to an ability, like "a has Hash".
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct AbleVariable {
pub variable: Variable,
pub name: Lowercase,
pub ability: Symbol,
// NB: there may be multiple occurrences of a variable
pub first_seen: Region,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct IntroducedVariables {
pub wildcards: Vec<Loc<Variable>>,
pub lambda_sets: Vec<Variable>,
pub inferred: Vec<Loc<Variable>>,
pub named: Vec<NamedVariable>,
pub able: Vec<AbleVariable>,
pub host_exposed_aliases: MutMap<Symbol, Variable>,
}
impl IntroducedVariables {
#[inline(always)]
fn debug_assert_not_already_present(&self, var: Variable) {
debug_assert!((self.wildcards.iter().map(|v| &v.value))
.chain(self.lambda_sets.iter())
.chain(self.inferred.iter().map(|v| &v.value))
.chain(self.named.iter().map(|nv| &nv.variable))
.chain(self.able.iter().map(|av| &av.variable))
.chain(self.host_exposed_aliases.values())
.all(|&v| v != var));
}
pub fn insert_named(&mut self, name: Lowercase, var: Loc<Variable>) {
debug_assert!(!self
.named
.iter()
.any(|nv| nv.name == name || nv.variable == var.value));
self.debug_assert_not_already_present(var.value);
let named_variable = NamedVariable {
name,
@ -52,19 +87,36 @@ impl IntroducedVariables {
self.named.push(named_variable);
}
pub fn insert_able(&mut self, name: Lowercase, var: Loc<Variable>, ability: Symbol) {
self.debug_assert_not_already_present(var.value);
let able_variable = AbleVariable {
name,
ability,
variable: var.value,
first_seen: var.region,
};
self.able.push(able_variable);
}
pub fn insert_wildcard(&mut self, var: Loc<Variable>) {
self.debug_assert_not_already_present(var.value);
self.wildcards.push(var);
}
pub fn insert_inferred(&mut self, var: Loc<Variable>) {
self.debug_assert_not_already_present(var.value);
self.inferred.push(var);
}
fn insert_lambda_set(&mut self, var: Variable) {
self.debug_assert_not_already_present(var);
self.lambda_sets.push(var);
}
pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) {
self.debug_assert_not_already_present(var);
self.host_exposed_aliases.insert(symbol, var);
}
@ -78,6 +130,10 @@ impl IntroducedVariables {
self.named.extend(other.named.iter().cloned());
self.named.sort();
self.named.dedup();
self.able.extend(other.able.iter().cloned());
self.able.sort();
self.able.dedup();
}
pub fn union_owned(&mut self, other: Self) {
@ -91,15 +147,26 @@ impl IntroducedVariables {
self.named.dedup();
}
pub fn var_by_name(&self, name: &Lowercase) -> Option<&Variable> {
self.named
.iter()
.find(|nv| &nv.name == name)
.map(|nv| &nv.variable)
pub fn var_by_name(&self, name: &Lowercase) -> Option<Variable> {
(self.named.iter().map(|nv| (&nv.name, nv.variable)))
.chain(self.able.iter().map(|av| (&av.name, av.variable)))
.find(|(cand, _)| cand == &name)
.map(|(_, var)| var)
}
pub fn named_var_by_name(&self, name: &Lowercase) -> Option<&NamedVariable> {
self.named.iter().find(|nv| &nv.name == name)
pub fn named_var_by_name(&self, name: &Lowercase) -> Option<NamedOrAbleVariable> {
if let Some(nav) = self
.named
.iter()
.find(|nv| &nv.name == name)
.map(|nv| NamedOrAbleVariable::Named(nv))
{
return Some(nav);
}
self.able
.iter()
.find(|av| &av.name == name)
.map(|av| NamedOrAbleVariable::Able(av))
}
}
@ -140,13 +207,6 @@ pub fn canonicalize_annotation(
}
}
#[derive(Clone, Debug)]
pub struct HasClause {
pub var_name: Lowercase,
pub var: Variable,
pub ability: Symbol,
}
pub fn canonicalize_annotation_with_possible_clauses(
env: &mut Env,
scope: &mut Scope,
@ -154,16 +214,17 @@ pub fn canonicalize_annotation_with_possible_clauses(
region: Region,
var_store: &mut VarStore,
abilities_in_scope: &[Symbol],
) -> (Annotation, Vec<Loc<HasClause>>) {
) -> Annotation {
let mut introduced_variables = IntroducedVariables::default();
let mut references = MutSet::default();
let mut aliases = SendMap::default();
let (annotation, region, clauses) = match annotation {
let (annotation, region) = match annotation {
TypeAnnotation::Where(annotation, clauses) => {
let mut can_clauses = Vec::with_capacity(clauses.len());
// Add each "has" clause. The association of a variable to an ability will be saved on
// `introduced_variables`, which we'll process later.
for clause in clauses.iter() {
match canonicalize_has_clause(
let opt_err = canonicalize_has_clause(
env,
scope,
var_store,
@ -171,24 +232,19 @@ pub fn canonicalize_annotation_with_possible_clauses(
clause,
abilities_in_scope,
&mut references,
) {
Ok(result) => can_clauses.push(Loc::at(clause.region, result)),
Err(err_type) => {
return (
Annotation {
typ: err_type,
introduced_variables,
references,
aliases,
},
can_clauses,
)
}
};
);
if let Err(err_type) = opt_err {
return Annotation {
typ: err_type,
introduced_variables,
references,
aliases,
};
}
}
(&annotation.value, annotation.region, can_clauses)
(&annotation.value, annotation.region)
}
annot => (annot, region, vec![]),
annot => (annot, region),
};
let typ = can_annotation_help(
@ -209,7 +265,7 @@ pub fn canonicalize_annotation_with_possible_clauses(
aliases,
};
(annot, clauses)
annot
}
fn make_apply_symbol(
@ -495,7 +551,7 @@ fn can_annotation_help(
let name = Lowercase::from(*v);
match introduced_variables.var_by_name(&name) {
Some(var) => Type::Variable(*var),
Some(var) => Type::Variable(var),
None => {
let var = var_store.fresh();
@ -559,8 +615,8 @@ fn can_annotation_help(
let var_name = Lowercase::from(var);
if let Some(var) = introduced_variables.var_by_name(&var_name) {
vars.push((var_name.clone(), Type::Variable(*var)));
lowercase_vars.push(Loc::at(loc_var.region, (var_name, *var)));
vars.push((var_name.clone(), Type::Variable(var)));
lowercase_vars.push(Loc::at(loc_var.region, (var_name, var)));
} else {
let var = var_store.fresh();
@ -792,7 +848,7 @@ fn canonicalize_has_clause(
clause: &Loc<roc_parse::ast::HasClause<'_>>,
abilities_in_scope: &[Symbol],
references: &mut MutSet<Symbol>,
) -> Result<HasClause, Type> {
) -> Result<(), Type> {
let Loc {
region,
value: roc_parse::ast::HasClause { var, ability },
@ -829,25 +885,21 @@ fn canonicalize_has_clause(
let var_name_ident = var_name.to_string().into();
let shadow = Loc::at(region, var_name_ident);
env.problem(roc_problem::can::Problem::Shadowing {
original_region: shadowing.first_seen,
original_region: shadowing.first_seen(),
shadow: shadow.clone(),
kind: ShadowKind::Variable,
});
return Err(Type::Erroneous(Problem::Shadowed(
shadowing.first_seen,
shadowing.first_seen(),
shadow,
)));
}
let var = var_store.fresh();
introduced_variables.insert_named(var_name.clone(), Loc::at(region, var));
introduced_variables.insert_able(var_name.clone(), Loc::at(region, var), ability);
Ok(HasClause {
var_name,
var,
ability,
})
Ok(())
}
#[allow(clippy::too_many_arguments)]
@ -1098,7 +1150,7 @@ fn can_assigned_fields<'a>(
let field_name = Lowercase::from(loc_field_name.value);
let field_type = {
if let Some(var) = introduced_variables.var_by_name(&field_name) {
Type::Variable(*var)
Type::Variable(var)
} else {
let field_var = var_store.fresh();
introduced_variables.insert_named(

View file

@ -1,4 +1,3 @@
use crate::abilities::AbilitiesStore;
use crate::annotation::canonicalize_annotation;
use crate::annotation::canonicalize_annotation_with_possible_clauses;
use crate::annotation::IntroducedVariables;
@ -430,12 +429,11 @@ pub fn canonicalize_defs<'a>(
}
// Now we can go through and resolve all pending abilities, to add them to scope.
let mut abilities_store = AbilitiesStore::default();
for (loc_ability_name, members) in abilities.into_values() {
let mut can_members = Vec::with_capacity(members.len());
for member in members {
let (member_annot, clauses) = canonicalize_annotation_with_possible_clauses(
let member_annot = canonicalize_annotation_with_possible_clauses(
env,
&mut scope,
&member.typ.value,
@ -450,13 +448,14 @@ pub fn canonicalize_defs<'a>(
output.references.referenced_type_defs.insert(symbol);
}
let name_region = member.name.region;
let member_name = member.name.extract_spaces().item;
let member_sym = match scope.introduce(
member_name.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
member.name.region,
name_region,
) {
Ok(sym) => sym,
Err((original_region, shadow, _new_symbol)) => {
@ -473,9 +472,11 @@ pub fn canonicalize_defs<'a>(
// What variables in the annotation are bound to the parent ability, and what variables
// are bound to some other ability?
let (variables_bound_to_ability, variables_bound_to_other_abilities): (Vec<_>, Vec<_>) =
clauses
.into_iter()
.partition(|has_clause| has_clause.value.ability == loc_ability_name.value);
member_annot
.introduced_variables
.able
.iter()
.partition(|av| av.ability == loc_ability_name.value);
let mut bad_has_clauses = false;
@ -485,18 +486,18 @@ pub fn canonicalize_defs<'a>(
env.problem(Problem::AbilityMemberMissingHasClause {
member: member_sym,
ability: loc_ability_name.value,
region: member.name.region,
region: name_region,
});
bad_has_clauses = true;
}
if !variables_bound_to_other_abilities.is_empty() {
// Disallow variables bound to other abilities, for now.
for bad_clause in variables_bound_to_other_abilities.iter() {
for bad_variable in variables_bound_to_other_abilities.iter() {
env.problem(Problem::AbilityMemberBindsExternalAbility {
member: member_sym,
ability: loc_ability_name.value,
region: bad_clause.region,
region: bad_variable.first_seen,
});
}
bad_has_clauses = true;
@ -507,17 +508,22 @@ pub fn canonicalize_defs<'a>(
continue;
}
let has_clauses = variables_bound_to_ability
.into_iter()
.map(|clause| clause.value)
.collect();
can_members.push((member_sym, member_annot.typ, has_clauses));
// The introduced variables are good; add them to the output.
output
.introduced_variables
.union(&member_annot.introduced_variables);
can_members.push((member_sym, name_region, var_store.fresh(), member_annot.typ));
}
// Store what symbols a type must define implementations for to have this ability.
abilities_store.register_ability(loc_ability_name.value, can_members);
scope
.abilities_store
.register_ability(loc_ability_name.value, can_members);
}
dbg!(&scope.abilities_store, pattern_type);
// Now that we have the scope completely assembled, and shadowing resolved,
// we're ready to canonicalize any body exprs.
@ -526,14 +532,7 @@ pub fn canonicalize_defs<'a>(
// once we've finished assembling the entire scope.
let mut pending_value_defs = Vec::with_capacity(value_defs.len());
for loc_def in value_defs.into_iter() {
match to_pending_value_def(
env,
var_store,
loc_def.value,
&mut scope,
&abilities_store,
pattern_type,
) {
match to_pending_value_def(env, var_store, loc_def.value, &mut scope, pattern_type) {
None => { /* skip */ }
Some((new_output, pending_def)) => {
// store the top-level defs, used to ensure that closures won't capture them
@ -1561,7 +1560,9 @@ pub fn can_defs_with_return<'a>(
// Now that we've collected all the references, check to see if any of the new idents
// we defined went unused by the return expression. If any were unused, report it.
for (symbol, region) in symbols_introduced {
if !output.references.has_value_lookup(symbol) && !output.references.has_type_lookup(symbol)
if !output.references.has_value_lookup(symbol)
&& !output.references.has_type_lookup(symbol)
&& !scope.abilities_store.is_specialization_name(symbol)
{
env.problem(Problem::UnusedDef(symbol, region));
}
@ -1772,7 +1773,6 @@ fn to_pending_value_def<'a>(
var_store: &mut VarStore,
def: &'a ast::ValueDef<'a>,
scope: &mut Scope,
abilities_store: &AbilitiesStore,
pattern_type: PatternType,
) -> Option<(Output, PendingValueDef<'a>)> {
use ast::ValueDef::*;
@ -1784,7 +1784,6 @@ fn to_pending_value_def<'a>(
env,
var_store,
scope,
abilities_store,
pattern_type,
&loc_pattern.value,
loc_pattern.region,
@ -1801,7 +1800,6 @@ fn to_pending_value_def<'a>(
env,
var_store,
scope,
abilities_store,
pattern_type,
&loc_pattern.value,
loc_pattern.region,
@ -1832,7 +1830,6 @@ fn to_pending_value_def<'a>(
env,
var_store,
scope,
abilities_store,
pattern_type,
&body_pattern.value,
body_pattern.region,

View file

@ -1083,6 +1083,7 @@ fn canonicalize_when_branch<'a>(
&& !branch_output.references.has_value_lookup(symbol)
&& !branch_output.references.has_type_lookup(symbol)
&& !original_scope.contains_symbol(symbol)
&& !scope.abilities_store.is_specialization_name(symbol)
{
env.problem(Problem::UnusedDef(symbol, *region));
}

View file

@ -1,3 +1,4 @@
use crate::abilities::AbilitiesStore;
use crate::def::{canonicalize_defs, sort_can_defs, Declaration, Def};
use crate::effect_module::HostedGeneratedFunctions;
use crate::env::Env;
@ -28,11 +29,13 @@ pub struct Module {
/// all aliases. `bool` indicates whether it is exposed
pub aliases: MutMap<Symbol, (bool, Alias)>,
pub rigid_variables: RigidVariables,
pub abilities_store: AbilitiesStore,
}
#[derive(Debug, Default)]
pub struct RigidVariables {
pub named: MutMap<Variable, Lowercase>,
pub able: MutMap<Variable, (Lowercase, Symbol)>,
pub wildcards: MutSet<Variable>,
}
@ -250,6 +253,7 @@ pub fn canonicalize_module_defs<'a>(
if !output.references.has_value_lookup(symbol)
&& !output.references.has_type_lookup(symbol)
&& !exposed_symbols.contains(&symbol)
&& !scope.abilities_store.is_specialization_name(symbol)
{
env.problem(Problem::UnusedDef(symbol, region));
}
@ -259,6 +263,12 @@ pub fn canonicalize_module_defs<'a>(
rigid_variables.named.insert(named.variable, named.name);
}
for able in output.introduced_variables.able {
rigid_variables
.able
.insert(able.variable, (able.name, able.ability));
}
for var in output.introduced_variables.wildcards {
rigid_variables.wildcards.insert(var.value);
}
@ -444,6 +454,10 @@ pub fn canonicalize_module_defs<'a>(
aliases.insert(symbol, alias);
}
for member in scope.abilities_store.root_ability_members().keys() {
exposed_but_not_defined.remove(member);
}
// By this point, all exposed symbols should have been removed from
// exposed_symbols and added to exposed_vars_by_symbol. If any were
// not, that means they were declared as exposed but there was

View file

@ -1,4 +1,3 @@
use crate::abilities::AbilitiesStore;
use crate::annotation::freshen_opaque_def;
use crate::env::Env;
use crate::expr::{canonicalize_expr, unescape_char, Expr, IntValue, Output};
@ -157,7 +156,6 @@ pub fn canonicalize_def_header_pattern<'a>(
env: &mut Env<'a>,
var_store: &mut VarStore,
scope: &mut Scope,
abilities_store: &AbilitiesStore,
pattern_type: PatternType,
pattern: &ast::Pattern<'a>,
region: Region,
@ -168,11 +166,10 @@ pub fn canonicalize_def_header_pattern<'a>(
match pattern {
// Identifiers that shadow ability members may appear (and may only appear) at the header of a def.
Identifier(name) => match scope.introduce_or_shadow_ability_member(
(*name).into(),
dbg!((*name).into()),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
abilities_store,
) {
Ok((symbol, shadowing_ability_member)) => {
output.references.bound_symbols.insert(symbol);

View file

@ -22,7 +22,7 @@ pub struct Scope {
pub aliases: SendMap<Symbol, Alias>,
/// The abilities currently in scope, and their implementors.
pub abilities: SendMap<Symbol, Region>,
pub abilities_store: AbilitiesStore,
/// The current module being processed. This will be used to turn
/// unqualified idents into Symbols.
@ -68,7 +68,7 @@ impl Scope {
symbols: SendMap::default(),
aliases,
// TODO(abilities): default abilities in scope
abilities: SendMap::default(),
abilities_store: AbilitiesStore::default(),
}
}
@ -247,16 +247,20 @@ impl Scope {
exposed_ident_ids: &IdentIds,
all_ident_ids: &mut IdentIds,
region: Region,
abilities_store: &AbilitiesStore,
) -> Result<(Symbol, Option<Symbol>), (Region, Loc<Ident>, Symbol)> {
match self.idents.get(&ident) {
match dbg!(self.idents.get(&ident)) {
Some(&(original_symbol, original_region)) => {
let shadow_ident_id = all_ident_ids.add(ident.clone());
let shadow_symbol = Symbol::new(self.home, shadow_ident_id);
self.symbols.insert(shadow_symbol, region);
if abilities_store.is_ability_member_name(original_symbol) {
if self
.abilities_store
.is_ability_member_name(dbg!(original_symbol))
{
self.abilities_store
.register_specializing_symbol(shadow_symbol, original_symbol);
// Add a symbol for the shadow, but don't re-associate the member name.
Ok((shadow_symbol, Some(original_symbol)))
} else {