Merge pull request #2917 from rtfeldman/move-exhaustiveness-checking

Moves exhaustiveness checking to type solving phase
This commit is contained in:
Richard Feldman 2022-04-25 20:55:05 -04:00 committed by GitHub
commit 6da39aa296
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1067 additions and 177 deletions

View file

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
roc_collections = { path = "../collections" }
roc_error_macros = { path = "../../error_macros" }
roc_exhaustive = { path = "../exhaustive" }
roc_region = { path = "../region" }
roc_module = { path = "../module" }
roc_types = { path = "../types" }

View file

@ -100,6 +100,7 @@ pub enum TypeError {
ErrorType,
Vec<IncompleteAbilityImplementation>,
),
Exhaustive(roc_exhaustive::Error),
}
use roc_types::types::Alias;
@ -775,7 +776,7 @@ fn solve(
copy
}
Eq(type_index, expectation_index, category_index, region) => {
Eq(roc_can::constraint::Eq(type_index, expectation_index, category_index, region)) => {
let category = &constraints.categories[category_index.index()];
let actual =
@ -1104,32 +1105,7 @@ fn solve(
let actual =
either_type_index_to_var(constraints, subs, rank, pools, aliases, *type_index);
let mut stack = vec![actual];
while let Some(var) = stack.pop() {
use {Content::*, FlatType::*};
let mut desc = subs.get(var);
if let Structure(TagUnion(tags, ext)) = desc.content {
if let Structure(EmptyTagUnion) = subs.get_content_without_compacting(ext) {
let new_ext = subs.fresh_unnamed_flex_var();
subs.set_rank(new_ext, desc.rank);
let new_union = Structure(TagUnion(tags, new_ext));
desc.content = new_union;
subs.set(var, desc);
}
// Also open up all nested tag unions.
let all_vars = tags.variables().into_iter();
stack.extend(all_vars.flat_map(|slice| subs[slice]).map(|var| subs[var]));
}
// Today, an "open" constraint doesn't affect any types
// other than tag unions. Recursive tag unions are constructed
// at a later time (during occurs checks after tag unions are
// resolved), so that's not handled here either.
// NB: Handle record types here if we add presence constraints
// to their type inference as well.
}
open_tag_union(subs, actual);
state
}
@ -1196,12 +1172,163 @@ fn solve(
}
}
}
&Exhaustive(eq, sketched_rows, context) => {
// A few cases:
// 1. Either condition or branch types already have a type error. In this case just
// propagate it.
// 2. Types are correct, but there are redundancies. In this case we want
// exhaustiveness checking to pull those out.
// 3. Condition and branch types are "almost equal", that is one or the other is
// only missing a few more tags. In this case we want to run
// exhaustiveness checking both ways, to see which one is missing tags.
// 4. Condition and branch types aren't "almost equal", this is just a normal type
// error.
let roc_can::constraint::Eq(
real_var,
expected_branches,
real_category,
real_region,
) = constraints.eq[eq.index()];
let real_var =
either_type_index_to_var(constraints, subs, rank, pools, aliases, real_var);
let expected_branches = &constraints.expectations[expected_branches.index()];
let branches_var =
type_to_var(subs, rank, pools, aliases, expected_branches.get_type_ref());
let real_content = subs.get_content_without_compacting(real_var);
let branches_content = subs.get_content_without_compacting(branches_var);
let already_have_error = matches!(
(real_content, branches_content),
(
Content::Error | Content::Structure(FlatType::Erroneous(_)),
_
) | (
_,
Content::Error | Content::Structure(FlatType::Erroneous(_))
)
);
let snapshot = subs.snapshot();
let outcome = unify(subs, real_var, branches_var, Mode::EQ);
let should_check_exhaustiveness;
match outcome {
Success {
vars,
must_implement_ability,
} => {
subs.commit_snapshot(snapshot);
introduce(subs, rank, pools, &vars);
if !must_implement_ability.is_empty() {
internal_error!("Didn't expect ability vars to land here");
}
// Case 1: unify error types, but don't check exhaustiveness.
// Case 2: run exhaustiveness to check for redundant branches.
should_check_exhaustiveness = !already_have_error;
}
Failure(..) => {
// Rollback and check for almost-equality.
subs.rollback_to(snapshot);
let almost_eq_snapshot = subs.snapshot();
// TODO: turn this on for bidirectional exhaustiveness checking
// open_tag_union(subs, real_var);
open_tag_union(subs, branches_var);
let almost_eq = matches!(
unify(subs, real_var, branches_var, Mode::EQ),
Success { .. }
);
subs.rollback_to(almost_eq_snapshot);
if almost_eq {
// Case 3: almost equal, check exhaustiveness.
should_check_exhaustiveness = true;
} else {
// Case 4: incompatible types, report type error.
// Re-run first failed unification to get the type diff.
match unify(subs, real_var, branches_var, Mode::EQ) {
Failure(vars, actual_type, expected_type, _bad_impls) => {
introduce(subs, rank, pools, &vars);
let real_category =
constraints.categories[real_category.index()].clone();
let problem = TypeError::BadExpr(
real_region,
real_category,
actual_type,
expected_branches.replace_ref(expected_type),
);
problems.push(problem);
should_check_exhaustiveness = false;
}
_ => internal_error!("Must be failure"),
}
}
}
BadType(vars, problem) => {
subs.commit_snapshot(snapshot);
introduce(subs, rank, pools, &vars);
problems.push(TypeError::BadType(problem));
should_check_exhaustiveness = false;
}
}
let sketched_rows = constraints.sketched_rows[sketched_rows.index()].clone();
if should_check_exhaustiveness {
let checked = roc_can::exhaustive::check(subs, sketched_rows, context);
if let Err(errors) = checked {
problems.extend(errors.into_iter().map(TypeError::Exhaustive));
}
}
state
}
};
}
state
}
fn open_tag_union(subs: &mut Subs, var: Variable) {
let mut stack = vec![var];
while let Some(var) = stack.pop() {
use {Content::*, FlatType::*};
let mut desc = subs.get(var);
if let Structure(TagUnion(tags, ext)) = desc.content {
if let Structure(EmptyTagUnion) = subs.get_content_without_compacting(ext) {
let new_ext = subs.fresh_unnamed_flex_var();
subs.set_rank(new_ext, desc.rank);
let new_union = Structure(TagUnion(tags, new_ext));
desc.content = new_union;
subs.set(var, desc);
}
// Also open up all nested tag unions.
let all_vars = tags.variables().into_iter();
stack.extend(all_vars.flat_map(|slice| subs[slice]).map(|var| subs[var]));
}
// Today, an "open" constraint doesn't affect any types
// other than tag unions. Recursive tag unions are constructed
// at a later time (during occurs checks after tag unions are
// resolved), so that's not handled here either.
// NB: Handle record types here if we add presence constraints
// to their type inference as well.
}
}
/// If a symbol claims to specialize an ability member, check that its solved type in fact
/// does specialize the ability, and record the specialization.
#[allow(clippy::too_many_arguments)]

View file

@ -2560,11 +2560,10 @@ mod solve_expr {
}
// this test is related to a bug where ext_var would have an incorrect rank.
// This match has duplicate cases, but that's not important because exhaustiveness happens
// after inference.
// This match has duplicate cases, but we ignore that.
#[test]
fn to_bit_record() {
infer_eq_without_problem(
infer_eq(
indoc!(
r#"
foo = \rec ->
@ -5261,95 +5260,123 @@ mod solve_expr {
{
u8: (\n ->
when n is
123u8 -> n),
123u8 -> n
_ -> n),
u16: (\n ->
when n is
123u16 -> n),
123u16 -> n
_ -> n),
u32: (\n ->
when n is
123u32 -> n),
123u32 -> n
_ -> n),
u64: (\n ->
when n is
123u64 -> n),
123u64 -> n
_ -> n),
u128: (\n ->
when n is
123u128 -> n),
123u128 -> n
_ -> n),
i8: (\n ->
when n is
123i8 -> n),
123i8 -> n
_ -> n),
i16: (\n ->
when n is
123i16 -> n),
123i16 -> n
_ -> n),
i32: (\n ->
when n is
123i32 -> n),
123i32 -> n
_ -> n),
i64: (\n ->
when n is
123i64 -> n),
123i64 -> n
_ -> n),
i128: (\n ->
when n is
123i128 -> n),
123i128 -> n
_ -> n),
nat: (\n ->
when n is
123nat -> n),
123nat -> n
_ -> n),
bu8: (\n ->
when n is
0b11u8 -> n),
0b11u8 -> n
_ -> n),
bu16: (\n ->
when n is
0b11u16 -> n),
0b11u16 -> n
_ -> n),
bu32: (\n ->
when n is
0b11u32 -> n),
0b11u32 -> n
_ -> n),
bu64: (\n ->
when n is
0b11u64 -> n),
0b11u64 -> n
_ -> n),
bu128: (\n ->
when n is
0b11u128 -> n),
0b11u128 -> n
_ -> n),
bi8: (\n ->
when n is
0b11i8 -> n),
0b11i8 -> n
_ -> n),
bi16: (\n ->
when n is
0b11i16 -> n),
0b11i16 -> n
_ -> n),
bi32: (\n ->
when n is
0b11i32 -> n),
0b11i32 -> n
_ -> n),
bi64: (\n ->
when n is
0b11i64 -> n),
0b11i64 -> n
_ -> n),
bi128: (\n ->
when n is
0b11i128 -> n),
0b11i128 -> n
_ -> n),
bnat: (\n ->
when n is
0b11nat -> n),
0b11nat -> n
_ -> n),
dec: (\n ->
when n is
123.0dec -> n),
123.0dec -> n
_ -> n),
f32: (\n ->
when n is
123.0f32 -> n),
123.0f32 -> n
_ -> n),
f64: (\n ->
when n is
123.0f64 -> n),
123.0f64 -> n
_ -> n),
fdec: (\n ->
when n is
123dec -> n),
123dec -> n
_ -> n),
ff32: (\n ->
when n is
123f32 -> n),
123f32 -> n
_ -> n),
ff64: (\n ->
when n is
123f64 -> n),
123f64 -> n
_ -> n),
}
"#
),
@ -5681,6 +5708,7 @@ mod solve_expr {
@Id (Id _ A) -> ""
@Id (Id _ B) -> ""
@Id (Id _ (C { a: "" })) -> ""
@Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness
"#
),
r#"Id [ A, B, C { a : Str }* ] -> Str"#,
@ -5700,6 +5728,7 @@ mod solve_expr {
@Id (Id _ A) -> ""
@Id (Id _ B) -> ""
@Id (Id _ (C { a: "" })) -> ""
@Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness
f
"#
@ -6134,7 +6163,7 @@ mod solve_expr {
),
&[
"ob : Bool",
"ob : [ False, True ]",
"ob : Bool",
"True : [ False, True ]",
"False : [ False, True ]",
],