Basic canonicalization and error checking for abilities

This commit is contained in:
Ayaz Hafiz 2022-04-06 17:20:37 -04:00 committed by Ayaz Hafiz
parent 23bc8aab49
commit 66ec1b4a84
11 changed files with 1097 additions and 157 deletions

View file

@ -0,0 +1,64 @@
use roc_collections::all::{MutMap, MutSet};
use roc_module::symbol::Symbol;
use roc_types::types::Type;
use crate::annotation::HasClause;
/// 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>,
}
/// 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)]
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.
#[allow(unused)]
ability_members: MutMap<Symbol, AbilityMemberData>,
/// Tuples of (type, member) specifying that `type` declares an implementation of an ability
/// member `member`.
#[allow(unused)]
declared_implementations: MutSet<(Symbol, Symbol)>,
}
impl AbilitiesStore {
pub fn register_ability(
&mut self,
ability: Symbol,
members: Vec<(Symbol, Type, Vec<HasClause>)>,
) {
let mut members_vec = Vec::with_capacity(members.len());
for (member, signature, bound_has_clauses) in members.into_iter() {
members_vec.push(member);
self.ability_members.insert(
member,
AbilityMemberData {
parent_ability: ability,
signature,
bound_has_clauses,
},
);
}
self.members_of_ability.insert(ability, members_vec);
}
pub fn register_implementation(&mut self, implementing_type: Symbol, ability_member: Symbol) {
self.declared_implementations
.insert((implementing_type, ability_member));
}
}

View file

@ -1,10 +1,9 @@
use crate::env::Env;
use crate::scope::Scope;
use roc_collections::all::{ImMap, MutMap, MutSet, SendMap};
use roc_error_macros::todo_abilities;
use roc_module::ident::{Ident, Lowercase, TagName};
use roc_module::symbol::{IdentIds, ModuleId, Symbol};
use roc_parse::ast::{AssignedField, Pattern, Tag, TypeAnnotation, TypeHeader};
use roc_parse::ast::{AssignedField, ExtractSpaces, Pattern, Tag, TypeAnnotation, TypeHeader};
use roc_region::all::{Loc, Region};
use roc_types::subs::{VarStore, Variable};
use roc_types::types::{
@ -104,6 +103,10 @@ impl IntroducedVariables {
.find(|nv| nv.variable == var)
.map(|nv| &nv.name)
}
pub fn named_var_by_name(&self, name: &Lowercase) -> Option<&NamedVariable> {
self.named.iter().find(|nv| &nv.name == name)
}
}
fn malformed(env: &mut Env, region: Region, name: &str) {
@ -143,6 +146,78 @@ 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,
annotation: &TypeAnnotation,
region: Region,
var_store: &mut VarStore,
abilities_in_scope: &[Symbol],
) -> (Annotation, Vec<Loc<HasClause>>) {
let mut introduced_variables = IntroducedVariables::default();
let mut references = MutSet::default();
let mut aliases = SendMap::default();
let (annotation, region, clauses) = match annotation {
TypeAnnotation::Where(annotation, clauses) => {
let mut can_clauses = Vec::with_capacity(clauses.len());
for clause in clauses.iter() {
match canonicalize_has_clause(
env,
scope,
var_store,
&mut introduced_variables,
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,
)
}
};
}
(&annotation.value, annotation.region, can_clauses)
}
annot => (annot, region, vec![]),
};
let typ = can_annotation_help(
env,
annotation,
region,
scope,
var_store,
&mut introduced_variables,
&mut aliases,
&mut references,
);
let annot = Annotation {
typ,
introduced_variables,
references,
aliases,
};
(annot, clauses)
}
fn make_apply_symbol(
env: &mut Env,
region: Region,
@ -271,7 +346,13 @@ pub fn find_type_def_symbols(
SpaceBefore(inner, _) | SpaceAfter(inner, _) => {
stack.push(inner);
}
Where(..) => todo_abilities!(),
Where(annotation, clauses) => {
stack.push(&annotation.value);
for has_clause in clauses.iter() {
stack.push(&has_clause.value.ability.value);
}
}
Inferred | Wildcard | Malformed(_) => {}
}
}
@ -685,7 +766,17 @@ fn can_annotation_help(
Type::Variable(var)
}
Where(..) => todo_abilities!(),
Where(_annotation, clauses) => {
debug_assert!(!clauses.is_empty());
// Has clauses are allowed only on the top level of an ability member signature (for
// now), which we handle elsewhere.
env.problem(roc_problem::can::Problem::IllegalHasClause {
region: Region::across_all(clauses.iter().map(|clause| &clause.region)),
});
return Type::Erroneous(Problem::CanonicalizationProblem);
}
Malformed(string) => {
malformed(env, region, string);
@ -698,6 +789,71 @@ fn can_annotation_help(
}
}
fn canonicalize_has_clause(
env: &mut Env,
scope: &mut Scope,
var_store: &mut VarStore,
introduced_variables: &mut IntroducedVariables,
clause: &Loc<roc_parse::ast::HasClause<'_>>,
abilities_in_scope: &[Symbol],
references: &mut MutSet<Symbol>,
) -> Result<HasClause, Type> {
let Loc {
region,
value: roc_parse::ast::HasClause { var, ability },
} = clause;
let region = *region;
let var_name = var.extract_spaces().item;
debug_assert!(
var_name.starts_with(char::is_lowercase),
"Vars should have been parsed as lowercase"
);
let var_name = Lowercase::from(var_name);
let ability = match ability.value {
TypeAnnotation::Apply(module_name, ident, _type_arguments) => {
let symbol = make_apply_symbol(env, ability.region, scope, module_name, ident)?;
if !abilities_in_scope.contains(&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);
if let Some(shadowing) = introduced_variables.named_var_by_name(&var_name) {
let var_name_ident = var_name.to_string().into();
let shadow = Loc::at(region, var_name_ident);
env.problem(roc_problem::can::Problem::ShadowingInAnnotation {
original_region: shadowing.first_seen,
shadow: shadow.clone(),
});
return Err(Type::Erroneous(Problem::Shadowed(
shadowing.first_seen,
shadow,
)));
}
let var = var_store.fresh();
introduced_variables.insert_named(var_name.clone(), Loc::at(region, var));
Ok(HasClause {
var_name,
var,
ability,
})
}
#[allow(clippy::too_many_arguments)]
fn can_extension_type<'a>(
env: &mut Env,

View file

@ -1,4 +1,6 @@
use crate::abilities::AbilitiesStore;
use crate::annotation::canonicalize_annotation;
use crate::annotation::canonicalize_annotation_with_possible_clauses;
use crate::annotation::IntroducedVariables;
use crate::env::Env;
use crate::expr::ClosureData;
@ -10,10 +12,11 @@ use crate::scope::create_alias;
use crate::scope::Scope;
use roc_collections::all::ImSet;
use roc_collections::all::{default_hasher, ImEntry, ImMap, MutMap, MutSet, SendMap};
use roc_error_macros::todo_abilities;
use roc_module::ident::Lowercase;
use roc_module::symbol::Symbol;
use roc_parse::ast;
use roc_parse::ast::AbilityMember;
use roc_parse::ast::ExtractSpaces;
use roc_parse::ast::TypeHeader;
use roc_parse::pattern::PatternType;
use roc_problem::can::{CycleEntry, Problem, RuntimeError};
@ -86,10 +89,19 @@ enum PendingTypeDef<'a> {
kind: AliasKind,
},
Ability {
name: Loc<Symbol>,
members: &'a [ast::AbilityMember<'a>],
},
/// An invalid alias, that is ignored in the rest of the pipeline
/// e.g. a shadowed alias, or a definition like `MyAlias 1 : Int`
/// with an incorrect pattern
InvalidAlias { kind: AliasKind },
/// An invalid ability, that is ignored in the rest of the pipeline.
/// E.g. a shadowed ability, or with a bad definition.
InvalidAbility,
}
// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check.
@ -239,9 +251,19 @@ pub fn canonicalize_defs<'a>(
env.home.register_debug_idents(&env.ident_ids);
}
let mut aliases = SendMap::default();
enum TypeDef<'a> {
AliasLike(
Loc<Symbol>,
Vec<Loc<Lowercase>>,
&'a Loc<ast::TypeAnnotation<'a>>,
AliasKind,
),
Ability(Loc<Symbol>, &'a [AbilityMember<'a>]),
}
let mut type_defs = MutMap::default();
let mut abilities_in_scope = Vec::new();
let mut alias_defs = MutMap::default();
let mut referenced_type_symbols = MutMap::default();
for pending_def in pending_type_defs.into_iter() {
@ -260,19 +282,55 @@ pub fn canonicalize_defs<'a>(
referenced_type_symbols.insert(name.value, referenced_symbols);
alias_defs.insert(name.value, (name, vars, ann, kind));
type_defs.insert(name.value, TypeDef::AliasLike(name, vars, ann, kind));
}
PendingTypeDef::Ability { name, members } => {
let mut referenced_symbols = Vec::with_capacity(2);
for member in members.iter() {
// Add the referenced type symbols of each member function. We need to make
// sure those are processed first before we resolve the whole ability
// definition.
referenced_symbols.extend(crate::annotation::find_type_def_symbols(
env.home,
&mut env.ident_ids,
&member.typ.value,
));
}
referenced_type_symbols.insert(name.value, referenced_symbols);
type_defs.insert(name.value, TypeDef::Ability(name, members));
abilities_in_scope.push(name.value);
}
PendingTypeDef::InvalidAlias { .. } | PendingTypeDef::InvalidAbility { .. } => { /* ignore */
}
PendingTypeDef::InvalidAlias { .. } => { /* ignore */ }
}
}
let sorted = sort_type_defs_before_introduction(referenced_type_symbols);
let mut aliases = SendMap::default();
let mut abilities = MutMap::default();
for type_name in sorted {
let (name, vars, ann, kind) = alias_defs.remove(&type_name).unwrap();
match type_defs.remove(&type_name).unwrap() {
TypeDef::AliasLike(name, vars, ann, kind) => {
let symbol = name.value;
let can_ann = canonicalize_annotation(env, &mut scope, &ann.value, ann.region, var_store);
let can_ann =
canonicalize_annotation(env, &mut scope, &ann.value, ann.region, var_store);
// Does this alias reference any abilities? For now, we don't permit that.
let ability_references = can_ann
.references
.iter()
.filter_map(|&ty_ref| abilities_in_scope.iter().find(|&&name| name == ty_ref))
.collect::<Vec<_>>();
if let Some(one_ability_ref) = ability_references.first() {
env.problem(Problem::AliasUsesAbility {
loc_name: name,
ability: **one_ability_ref,
});
}
// Record all the annotation's references in output.references.lookups
for symbol in can_ann.references {
@ -349,6 +407,14 @@ pub fn canonicalize_defs<'a>(
aliases.insert(symbol, alias.clone());
}
TypeDef::Ability(name, members) => {
// For now we enforce that aliases cannot reference abilities, so let's wait to
// resolve ability definitions until aliases are resolved and in scope below.
abilities.insert(name.value, (name, members));
}
}
}
// Now that we know the alias dependency graph, we can try to insert recursion variables
// where aliases are recursive tag unions, or detect illegal recursions.
let mut aliases = correct_mutual_recursive_type_alias(env, aliases, var_store);
@ -362,6 +428,94 @@ 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(
env,
&mut scope,
&member.typ.value,
member.typ.region,
var_store,
&abilities_in_scope,
);
// Record all the annotation's references in output.references.lookups
for symbol in member_annot.references {
output.references.type_lookups.insert(symbol);
output.references.referenced_type_defs.insert(symbol);
}
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,
) {
Ok(sym) => sym,
Err((original_region, shadow, _new_symbol)) => {
env.problem(roc_problem::can::Problem::ShadowingInAnnotation {
original_region,
shadow,
});
// Pretend the member isn't a part of the ability
continue;
}
};
// 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);
let mut bad_has_clauses = false;
if variables_bound_to_ability.is_empty() {
// There are no variables bound to the parent ability - then this member doesn't
// need to be a part of the ability.
env.problem(Problem::AbilityMemberMissingHasClause {
member: member_sym,
ability: loc_ability_name.value,
region: member.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() {
env.problem(Problem::AbilityMemberBindsExternalAbility {
member: member_sym,
ability: loc_ability_name.value,
region: bad_clause.region,
});
}
bad_has_clauses = true;
}
if bad_has_clauses {
// Pretend the member isn't a part of the ability
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));
}
// Store what symbols a type must define implementations for to have this ability.
abilities_store.register_ability(loc_ability_name.value, can_members);
}
// Now that we have the scope completely assembled, and shadowing resolved,
// we're ready to canonicalize any body exprs.
@ -1551,7 +1705,46 @@ fn to_pending_type_def<'a>(
}
}
Ability { .. } => todo_abilities!(),
Ability {
header: TypeHeader { name, vars },
members,
loc_has: _,
} => {
let name = match scope.introduce_without_shadow_symbol(
name.value.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
name.region,
) {
Ok(symbol) => Loc::at(name.region, symbol),
Err((original_region, shadowed_symbol)) => {
env.problem(Problem::ShadowingInAnnotation {
original_region,
shadow: shadowed_symbol,
});
return Some((Output::default(), PendingTypeDef::InvalidAbility));
}
};
if !vars.is_empty() {
// Disallow ability type arguments, at least for now.
let variables_region = Region::across_all(vars.iter().map(|v| &v.region));
env.problem(Problem::AbilityHasTypeVariables {
name: name.value,
variables_region,
});
return Some((Output::default(), PendingTypeDef::InvalidAbility));
}
let pending_ability = PendingTypeDef::Ability {
name,
// We'll handle adding the member symbols later on when we do all value defs.
members,
};
Some((Output::default(), pending_ability))
}
}
}

View file

@ -1,6 +1,7 @@
#![warn(clippy::dbg_macro)]
// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check.
#![allow(clippy::large_enum_variant)]
pub mod abilities;
pub mod annotation;
pub mod builtins;
pub mod constraint;

View file

@ -19,6 +19,9 @@ pub struct Scope {
/// The type aliases currently in scope
pub aliases: SendMap<Symbol, Alias>,
/// The abilities currently in scope, and their implementors.
pub abilities: SendMap<Symbol, Region>,
/// The current module being processed. This will be used to turn
/// unqualified idents into Symbols.
home: ModuleId,
@ -62,6 +65,8 @@ impl Scope {
idents: Symbol::default_in_scope(),
symbols: SendMap::default(),
aliases,
// TODO(abilities): default abilities in scope
abilities: SendMap::default(),
}
}
@ -176,6 +181,11 @@ impl Scope {
///
/// Returns Err if this would shadow an existing ident, including the
/// Symbol and Region of the ident we already had in scope under that name.
///
/// If this ident shadows an existing one, a new ident is allocated for the shadow. This is
/// done so that all identifiers have unique symbols, which is important in particular when
/// we generate code for value identifiers.
/// If this behavior is undesirable, use [`Self::introduce_without_shadow_symbol`].
pub fn introduce(
&mut self,
ident: Ident,
@ -198,7 +208,37 @@ impl Scope {
Err((original_region, shadow, symbol))
}
None => {
None => Ok(self.commit_introduction(ident, exposed_ident_ids, all_ident_ids, region)),
}
}
/// Like [Self::introduce], but does not introduce a new symbol for the shadowing symbol.
pub fn introduce_without_shadow_symbol(
&mut self,
ident: Ident,
exposed_ident_ids: &IdentIds,
all_ident_ids: &mut IdentIds,
region: Region,
) -> Result<Symbol, (Region, Loc<Ident>)> {
match self.idents.get(&ident) {
Some(&(_, original_region)) => {
let shadow = Loc {
value: ident.clone(),
region,
};
Err((original_region, shadow))
}
None => Ok(self.commit_introduction(ident, exposed_ident_ids, all_ident_ids, region)),
}
}
fn commit_introduction(
&mut self,
ident: Ident,
exposed_ident_ids: &IdentIds,
all_ident_ids: &mut IdentIds,
region: Region,
) -> Symbol {
// If this IdentId was already added previously
// when the value was exposed in the module header,
// use that existing IdentId. Otherwise, create a fresh one.
@ -212,9 +252,7 @@ impl Scope {
self.symbols.insert(symbol, region);
self.idents.insert(ident, (symbol, region));
Ok(symbol)
}
}
symbol
}
/// Ignore an identifier.

View file

@ -866,24 +866,26 @@ fn parse_defs_end<'a>(
}
Ok((_, loc_pattern, state)) => {
// First let's check whether this is an ability definition.
if let Loc {
value:
let opt_tag_and_args: Option<(&str, Region, &[Loc<Pattern>])> = match loc_pattern.value
{
Pattern::Apply(
loc_name @ Loc {
Loc {
value: Pattern::GlobalTag(name),
..
region,
},
args,
),
..
} = loc_pattern
{
) => Some((name, *region, args)),
Pattern::GlobalTag(name) => Some((name, loc_pattern.region, &[])),
_ => None,
};
if let Some((name, name_region, args)) = opt_tag_and_args {
if let Ok((_, loc_has, state)) =
loc_has_parser(min_indent).parse(arena, state.clone())
{
let (_, loc_def, state) = finish_parsing_ability_def(
start_column,
Loc::at(loc_name.region, name),
Loc::at(name_region, name),
args,
loc_has,
arena,

View file

@ -33,16 +33,21 @@ Defs(
},
],
),
},
],
},
),
],
@35-71 SpaceBefore(
Defs(
[
@35-68 Type(
Ability {
@24-33 HasClause {
var: @24-25 "a",
ability: @30-33 Apply(
"",
"Ab1",
[],
),
},
],
),
},
],
},
@35-68 Ability {
header: TypeHeader {
name: @35-38 "Ab2",
vars: [],
@ -77,7 +82,6 @@ Defs(
},
],
},
),
],
@70-71 SpaceBefore(
Num(
@ -88,10 +92,4 @@ Defs(
Newline,
],
),
),
[
Newline,
Newline,
],
),
)

View file

@ -95,6 +95,30 @@ pub enum Problem {
region: Region,
kind: ExtensionTypeKind,
},
AbilityHasTypeVariables {
name: Symbol,
variables_region: Region,
},
HasClauseIsNotAbility {
region: Region,
},
IllegalHasClause {
region: Region,
},
AbilityMemberMissingHasClause {
member: Symbol,
ability: Symbol,
region: Region,
},
AbilityMemberBindsExternalAbility {
member: Symbol,
ability: Symbol,
region: Region,
},
AliasUsesAbility {
loc_name: Loc<Symbol>,
ability: Symbol,
},
}
#[derive(Clone, Debug, PartialEq)]

View file

@ -1856,6 +1856,7 @@ pub enum Problem {
},
InvalidModule,
SolvedTypeError,
HasClauseIsNotAbility(Region),
}
#[derive(PartialEq, Eq, Debug, Clone)]

View file

@ -38,6 +38,12 @@ const OPAQUE_DECLARED_OUTSIDE_SCOPE: &str = "OPAQUE TYPE DECLARED OUTSIDE SCOPE"
const OPAQUE_NOT_APPLIED: &str = "OPAQUE TYPE NOT APPLIED";
const OPAQUE_OVER_APPLIED: &str = "OPAQUE TYPE APPLIED TO TOO MANY ARGS";
const INVALID_EXTENSION_TYPE: &str = "INVALID_EXTENSION_TYPE";
const ABILITY_HAS_TYPE_VARIABLES: &str = "ABILITY HAS TYPE VARIABLES";
const HAS_CLAUSE_IS_NOT_AN_ABILITY: &str = "HAS CLAUSE IS NOT AN ABILITY";
const ALIAS_USES_ABILITY: &str = "ALIAS USES ABILITY";
const ILLEGAL_HAS_CLAUSE: &str = "ILLEGAL HAS CLAUSE";
const ABILITY_MEMBER_MISSING_HAS_CLAUSE: &str = "ABILITY MEMBER MISSING HAS CLAUSE";
const ABILITY_MEMBER_HAS_EXTRANEOUS_HAS_CLAUSE: &str = "ABILITY MEMBER HAS EXTRANEOUS HAS CLAUSE";
pub fn can_problem<'b>(
alloc: &'b RocDocAllocator<'b>,
@ -562,6 +568,134 @@ pub fn can_problem<'b>(
title = INVALID_EXTENSION_TYPE.to_string();
severity = Severity::RuntimeError;
}
Problem::AbilityHasTypeVariables {
name,
variables_region,
} => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("The definition of the "),
alloc.symbol_unqualified(name),
alloc.reflow(" ability includes type variables:"),
]),
alloc.region(lines.convert_region(variables_region)),
alloc.reflow(
"Abilities cannot depend on type variables, but their member values can!",
),
]);
title = ABILITY_HAS_TYPE_VARIABLES.to_string();
severity = Severity::RuntimeError;
}
Problem::HasClauseIsNotAbility {
region: clause_region,
} => {
doc = alloc.stack(vec![
alloc.reflow(r#"The type referenced in this "has" clause is not an ability:"#),
alloc.region(lines.convert_region(clause_region)),
]);
title = HAS_CLAUSE_IS_NOT_AN_ABILITY.to_string();
severity = Severity::RuntimeError;
}
Problem::AliasUsesAbility {
loc_name: Loc {
region,
value: name,
},
ability,
} => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("The definition of the "),
alloc.symbol_unqualified(name),
alloc.reflow(" aliases references the ability "),
alloc.symbol_unqualified(ability),
alloc.reflow(":"),
]),
alloc.region(lines.convert_region(region)),
]);
title = ALIAS_USES_ABILITY.to_string();
severity = Severity::RuntimeError;
}
Problem::IllegalHasClause { region } => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("A "),
alloc.keyword("has"),
alloc.reflow(" clause is not allowed here:"),
]),
alloc.region(lines.convert_region(region)),
alloc.concat(vec![
alloc.keyword("has"),
alloc.reflow(" clauses can only be specified on the top-level type annotation of an ability member."),
]),
]);
title = ILLEGAL_HAS_CLAUSE.to_string();
severity = Severity::RuntimeError;
}
Problem::AbilityMemberMissingHasClause {
member,
ability,
region,
} => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("The definition of the ability member "),
alloc.symbol_unqualified(member),
alloc.reflow(" does not include a "),
alloc.keyword("has"),
alloc.reflow(" clause binding a type variable to the ability "),
alloc.symbol_unqualified(ability),
alloc.reflow(":"),
]),
alloc.region(lines.convert_region(region)),
alloc.concat(vec![
alloc.reflow("Ability members must include a "),
alloc.keyword("has"),
alloc.reflow(" clause binding a type variable to an ability, like"),
]),
alloc.type_block(alloc.concat(vec![
alloc.type_variable("a".into()),
alloc.space(),
alloc.keyword("has"),
alloc.space(),
alloc.symbol_unqualified(ability),
])),
alloc.concat(vec![alloc.reflow(
"Otherwise, the function does not need to be part of the ability!",
)]),
]);
title = ABILITY_MEMBER_MISSING_HAS_CLAUSE.to_string();
severity = Severity::RuntimeError;
}
Problem::AbilityMemberBindsExternalAbility {
member,
ability,
region,
} => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("The definition of the ability member "),
alloc.symbol_unqualified(member),
alloc.reflow(" includes a has clause binding an ability it is not a part of:"),
]),
alloc.region(lines.convert_region(region)),
alloc.reflow("Currently, ability members can only bind variables to the ability they are a part of."),
alloc.concat(vec![
alloc.hint(""),
alloc.reflow("Did you mean to bind the "),
alloc.symbol_unqualified(ability),
alloc.reflow(" ability instead?"),
]),
]);
title = ABILITY_MEMBER_HAS_EXTRANEOUS_HAS_CLAUSE.to_string();
severity = Severity::RuntimeError;
}
};
Report {

View file

@ -8896,4 +8896,333 @@ I need all branches in an `if` to have the same type!
),
)
}
#[test]
fn bad_type_parameter_in_ability() {
report_problem_as(
indoc!(
r#"
Hash a b c has
hash : a -> U64 | a has Hash
1
"#
),
indoc!(
r#"
ABILITY HAS TYPE VARIABLES
The definition of the `Hash` ability includes type variables:
1 Hash a b c has
^^^^^
Abilities cannot depend on type variables, but their member values
can!
UNUSED DEFINITION
`Hash` is not used anywhere in your code.
1 Hash a b c has
^^^^
If you didn't intend on using `Hash` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn alias_in_has_clause() {
report_problem_as(
indoc!(
r#"
Hash has hash : a, b -> U64 | a has Hash, b has Bool
1
"#
),
indoc!(
r#"
HAS CLAUSE IS NOT AN ABILITY
The type referenced in this "has" clause is not an ability:
1 Hash has hash : a, b -> U64 | a has Hash, b has Bool
^^^^
UNUSED DEFINITION
`hash` is not used anywhere in your code.
1 Hash has hash : a, b -> U64 | a has Hash, b has Bool
^^^^
If you didn't intend on using `hash` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn shadowed_type_variable_in_has_clause() {
report_problem_as(
indoc!(
r#"
Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1
1
"#
),
indoc!(
r#"
DUPLICATE NAME
The `a` name is first defined here:
1 Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1
^^^^^^^^^
But then it's defined a second time here:
1 Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1
^^^^^^^^^
Since these variables have the same name, it's easy to use the wrong
one on accident. Give one of them a new name.
UNUSED DEFINITION
`ab1` is not used anywhere in your code.
1 Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1
^^^
If you didn't intend on using `ab1` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn alias_using_ability() {
report_problem_as(
indoc!(
r#"
Ability has ab : a -> {} | a has Ability
Alias : Ability
a : Alias
a
"#
),
indoc!(
r#"
ALIAS USES ABILITY
The definition of the `Alias` aliases references the ability `Ability`:
3 Alias : Ability
^^^^^
UNUSED DEFINITION
`ab` is not used anywhere in your code.
1 Ability has ab : a -> {} | a has Ability
^^
If you didn't intend on using `ab` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn ability_shadows_ability() {
report_problem_as(
indoc!(
r#"
Ability has ab : a -> U64 | a has Ability
Ability has ab1 : a -> U64 | a has Ability
1
"#
),
indoc!(
r#"
DUPLICATE NAME
The `Ability` name is first defined here:
1 Ability has ab : a -> U64 | a has Ability
^^^^^^^
But then it's defined a second time here:
3 Ability has ab1 : a -> U64 | a has Ability
^^^^^^^
Since these variables have the same name, it's easy to use the wrong
one on accident. Give one of them a new name.
UNUSED DEFINITION
`ab` is not used anywhere in your code.
1 Ability has ab : a -> U64 | a has Ability
^^
If you didn't intend on using `ab` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn ability_member_does_not_bind_ability() {
report_problem_as(
indoc!(
r#"
Ability has ab : {} -> {}
1
"#
),
indoc!(
r#"
ABILITY MEMBER MISSING HAS CLAUSE
The definition of the ability member `ab` does not include a `has` clause
binding a type variable to the ability `Ability`:
1 Ability has ab : {} -> {}
^^
Ability members must include a `has` clause binding a type variable to
an ability, like
a has Ability
Otherwise, the function does not need to be part of the ability!
UNUSED DEFINITION
`Ability` is not used anywhere in your code.
1 Ability has ab : {} -> {}
^^^^^^^
If you didn't intend on using `Ability` then remove it so future readers
of your code don't wonder why it is there.
UNUSED DEFINITION
`ab` is not used anywhere in your code.
1 Ability has ab : {} -> {}
^^
If you didn't intend on using `ab` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn ability_member_binds_extra_ability() {
report_problem_as(
indoc!(
r#"
Eq has eq : a, a -> Bool | a has Eq
Hash has hash : a, b -> U64 | a has Eq, b has Hash
1
"#
),
indoc!(
r#"
ABILITY MEMBER HAS EXTRANEOUS HAS CLAUSE
The definition of the ability member `hash` includes a has clause
binding an ability it is not a part of:
2 Hash has hash : a, b -> U64 | a has Eq, b has Hash
^^^^^^^^
Currently, ability members can only bind variables to the ability they
are a part of.
Hint: Did you mean to bind the `Hash` ability instead?
UNUSED DEFINITION
`eq` is not used anywhere in your code.
1 Eq has eq : a, a -> Bool | a has Eq
^^
If you didn't intend on using `eq` then remove it so future readers of
your code don't wonder why it is there.
UNUSED DEFINITION
`hash` is not used anywhere in your code.
2 Hash has hash : a, b -> U64 | a has Eq, b has Hash
^^^^
If you didn't intend on using `hash` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
#[test]
fn has_clause_outside_of_ability() {
report_problem_as(
indoc!(
r#"
Hash has hash : a -> U64 | a has Hash
f : a -> U64 | a has Hash
f
"#
),
indoc!(
r#"
ILLEGAL HAS CLAUSE
A `has` clause is not allowed here:
3 f : a -> U64 | a has Hash
^^^^^^^^^^
`has` clauses can only be specified on the top-level type annotation of
an ability member.
UNUSED DEFINITION
`hash` is not used anywhere in your code.
1 Hash has hash : a -> U64 | a has Hash
^^^^
If you didn't intend on using `hash` then remove it so future readers of
your code don't wonder why it is there.
"#
),
)
}
}