Using abilities as types is illegal, but we can still compile them

Closes #2881
This commit is contained in:
Ayaz Hafiz 2022-04-19 16:41:18 -04:00
parent c0dec1d5bc
commit 80dc50763e
No known key found for this signature in database
GPG Key ID: 0E2A37416A25EF58
7 changed files with 151 additions and 4 deletions

View File

@ -85,6 +85,10 @@ impl AbilitiesStore {
);
}
pub fn is_ability(&self, ability: Symbol) -> bool {
self.members_of_ability.contains_key(&ability)
}
/// 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.

View File

@ -8,7 +8,8 @@ use roc_problem::can::ShadowKind;
use roc_region::all::{Loc, Region};
use roc_types::subs::{VarStore, Variable};
use roc_types::types::{
Alias, AliasCommon, AliasKind, LambdaSet, Problem, RecordField, Type, TypeExtension,
name_type_var, Alias, AliasCommon, AliasKind, LambdaSet, Problem, RecordField, Type,
TypeExtension,
};
#[derive(Clone, Debug, PartialEq)]
@ -397,6 +398,16 @@ pub fn find_type_def_symbols(
result
}
/// Generates a fresh type variable name. PERF: not super performant, don't prefer using this in
/// non-degenerate compilation paths!
fn find_fresh_var_name(introduced_variables: &IntroducedVariables) -> Lowercase {
let mut taken = introduced_variables
.iter_named()
.map(|v| v.name().clone())
.collect();
name_type_var(0, &mut taken).0
}
#[allow(clippy::too_many_arguments)]
fn can_annotation_help(
env: &mut Env,
@ -418,7 +429,7 @@ fn can_annotation_help(
let arg_ann = can_annotation_help(
env,
&arg.value,
region,
arg.region,
scope,
var_store,
introduced_variables,
@ -456,6 +467,21 @@ fn can_annotation_help(
references.insert(symbol);
if scope.abilities_store.is_ability(symbol) {
let fresh_ty_var = find_fresh_var_name(introduced_variables);
env.problem(roc_problem::can::Problem::AbilityUsedAsType(
fresh_ty_var.clone(),
symbol,
region,
));
// Generate an variable bound to the ability so we can keep compiling.
let var = var_store.fresh();
introduced_variables.insert_able(fresh_ty_var, Loc::at(region, var), symbol);
return Type::Variable(var);
}
for arg in *type_arguments {
let arg_ann = can_annotation_help(
env,

View File

@ -138,6 +138,7 @@ pub enum Problem {
AbilityNotOnToplevel {
region: Region,
},
AbilityUsedAsType(Lowercase, Symbol, Region),
}
#[derive(Clone, Debug, PartialEq)]

View File

@ -188,3 +188,31 @@ fn ability_constrained_in_non_member_multiple_specializations_inferred() {
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_used_as_type_still_compiles() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [ result ] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes : Hash, Hash -> U64
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \$Id n -> n
Three := {}
hash = \$Three _ -> 3
result = mulHashes ($Id 100) ($Three {})
"#
),
300,
u64
)
}

View File

@ -106,8 +106,9 @@ fn create_llvm_module<'a>(
use roc_problem::can::Problem::*;
for problem in can_problems.into_iter() {
// Ignore "unused" problems
dbg!(&problem);
match problem {
// Ignore "unused" problems
UnusedDef(_, _)
| UnusedArgument(_, _, _)
| UnusedImport(_, _)
@ -122,6 +123,8 @@ fn create_llvm_module<'a>(
delayed_errors.push(buf.clone());
lines.push(buf);
}
// We should be able to compile even when abilities are used as types
AbilityUsedAsType(..) => {}
_ => {
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();

View File

@ -46,6 +46,7 @@ const ABILITY_MEMBER_MISSING_HAS_CLAUSE: &str = "ABILITY MEMBER MISSING HAS CLAU
const ABILITY_MEMBER_HAS_EXTRANEOUS_HAS_CLAUSE: &str = "ABILITY MEMBER HAS EXTRANEOUS HAS CLAUSE";
const ABILITY_MEMBER_BINDS_MULTIPLE_VARIABLES: &str = "ABILITY MEMBER BINDS MULTIPLE VARIABLES";
const ABILITY_NOT_ON_TOPLEVEL: &str = "ABILITY NOT ON TOP-LEVEL";
const ABILITY_USED_AS_TYPE: &str = "ABILITY USED AS TYPE";
pub fn can_problem<'b>(
alloc: &'b RocDocAllocator<'b>,
@ -750,6 +751,34 @@ pub fn can_problem<'b>(
title = ABILITY_NOT_ON_TOPLEVEL.to_string();
severity = Severity::RuntimeError;
}
Problem::AbilityUsedAsType(suggested_var_name, ability, region) => {
doc = alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("You are attempting to use the ability "),
alloc.symbol_unqualified(ability),
alloc.reflow(" as a type directly:"),
]),
alloc.region(lines.convert_region(region)),
alloc.reflow(
"Abilities can only be used in type annotations to constrain type variables.",
),
alloc
.hint("")
.append(alloc.reflow("Perhaps you meant to include a "))
.append(alloc.keyword("has"))
.append(alloc.reflow(" annotation, like")),
alloc.type_block(alloc.concat(vec![
alloc.type_variable(suggested_var_name),
alloc.space(),
alloc.keyword("has"),
alloc.space(),
alloc.symbol_unqualified(ability),
])),
]);
title = ABILITY_USED_AS_TYPE.to_string();
severity = Severity::RuntimeError;
}
};
Report {

View File

@ -8877,7 +8877,7 @@ I need all branches in an `if` to have the same type!
I cannot find a `UnknownType` value
3 insertHelper : UnknownType, Type -> Type
^^^^
^^^^^^^^^^^
Did you mean one of these?
@ -9873,4 +9873,60 @@ I need all branches in an `if` to have the same type!
),
)
}
#[test]
fn ability_value_annotations_are_an_error() {
new_report_problem_as(
indoc!(
r#"
app "test" provides [ result ] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes : Hash, Hash -> U64
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \$Id n -> n
Three := {}
hash = \$Three _ -> 3
result = mulHashes ($Id 100) ($Three {})
"#
),
indoc!(
r#"
ABILITY USED AS TYPE
You are attempting to use the ability `Hash` as a type directly:
6 mulHashes : Hash, Hash -> U64
^^^^
Abilities can only be used in type annotations to constrain type
variables.
Hint: Perhaps you meant to include a `has` annotation, like
a has Hash
ABILITY USED AS TYPE
You are attempting to use the ability `Hash` as a type directly:
6 mulHashes : Hash, Hash -> U64
^^^^
Abilities can only be used in type annotations to constrain type
variables.
Hint: Perhaps you meant to include a `has` annotation, like
b has Hash
"#
),
)
}
}