Move exhaustiveness checking to type checking

This commit is contained in:
Ayaz Hafiz 2022-04-22 15:23:56 -04:00
parent f7e04490c0
commit 85e3373d8b
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
15 changed files with 346 additions and 94 deletions

View file

@ -1197,12 +1197,116 @@ fn solve(
}
}
}
Exhaustive(rows_index, context) => {
let sketched_rows = constraints.sketched_rows[rows_index.index()].clone();
&Exhaustive {
real_var,
real_region,
real_category,
expected_branches,
sketched_rows,
context,
} => {
// A few cases:
// 1. Either condition or branch types already have a type error. In this case just
// propogate 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 checked = roc_can::exhaustive::check(&subs, sketched_rows, *context);
if let Err(errors) = checked {
problems.extend(errors.into_iter().map(TypeError::Exhaustive));
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 = match (real_content, branches_content) {
(Content::Error | Content::Structure(FlatType::Erroneous(_)), _) => true,
(_, Content::Error | Content::Structure(FlatType::Erroneous(_))) => true,
_ => false,
};
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
@ -1213,6 +1317,35 @@ fn solve(
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)]