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

@ -4727,6 +4727,7 @@ fn result_map(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(
@ -4824,6 +4825,7 @@ fn result_map_err(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(
@ -4887,6 +4889,7 @@ fn result_with_default(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(
@ -4964,6 +4967,7 @@ fn result_is_err(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(
@ -5041,6 +5045,7 @@ fn result_is_ok(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(
@ -5133,6 +5138,7 @@ fn result_after(symbol: Symbol, var_store: &mut VarStore) -> Def {
region: Region::zero(),
loc_cond: Box::new(no_region(Var(Symbol::ARG_1))),
branches,
branches_cond_var: var_store.fresh(),
};
defn(

View file

@ -1,3 +1,4 @@
use crate::exhaustive::{ExhaustiveContext, SketchedRows};
use crate::expected::{Expected, PExpected};
use roc_collections::soa::{EitherIndex, Index, Slice};
use roc_module::ident::TagName;
@ -19,6 +20,8 @@ pub struct Constraints {
pub pattern_expectations: Vec<PExpected<Type>>,
pub includes_tags: Vec<IncludesTag>,
pub strings: Vec<&'static str>,
pub sketched_rows: Vec<SketchedRows>,
pub eq: Vec<Eq>,
}
impl Default for Constraints {
@ -40,6 +43,8 @@ impl Constraints {
let pattern_expectations = Vec::new();
let includes_tags = Vec::new();
let strings = Vec::new();
let sketched_rows = Vec::new();
let eq = Vec::new();
types.extend([
Type::EmptyRec,
@ -90,6 +95,8 @@ impl Constraints {
pattern_expectations,
includes_tags,
strings,
sketched_rows,
eq,
}
}
@ -225,7 +232,7 @@ impl Constraints {
let expected_index = Index::push_new(&mut self.expectations, expected);
let category_index = Self::push_category(self, category);
Constraint::Eq(type_index, expected_index, category_index, region)
Constraint::Eq(Eq(type_index, expected_index, category_index, region))
}
#[inline(always)]
@ -240,7 +247,7 @@ impl Constraints {
let expected_index = Index::push_new(&mut self.expectations, expected);
let category_index = Self::push_category(self, category);
Constraint::Eq(type_index, expected_index, category_index, region)
Constraint::Eq(Eq(type_index, expected_index, category_index, region))
}
#[inline(always)]
@ -256,17 +263,17 @@ impl Constraints {
let expected_index = Index::push_new(&mut self.expectations, expected);
let category_index = Self::push_category(self, category);
let equal = Constraint::Eq(type_index, expected_index, category_index, region);
let equal = Constraint::Eq(Eq(type_index, expected_index, category_index, region));
let storage_type_index = Self::push_type_variable(storage_var);
let storage_category = Category::Storage(std::file!(), std::line!());
let storage_category_index = Self::push_category(self, storage_category);
let storage = Constraint::Eq(
let storage = Constraint::Eq(Eq(
storage_type_index,
expected_index,
storage_category_index,
region,
);
));
self.and_constraint([equal, storage])
}
@ -544,11 +551,6 @@ impl Constraints {
pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool {
match constraint {
Constraint::Eq(..) => false,
Constraint::Store(..) => false,
Constraint::Lookup(..) => false,
Constraint::Pattern(..) => false,
Constraint::True => false,
Constraint::SaveTheEnvironment => true,
Constraint::Let(index, _) => {
let let_constraint = &self.let_constraints[index.index()];
@ -567,9 +569,15 @@ impl Constraints {
.iter()
.any(|c| self.contains_save_the_environment(c))
}
Constraint::IsOpenType(_) => false,
Constraint::IncludesTag(_) => false,
Constraint::PatternPresence(_, _, _, _) => false,
Constraint::Eq(..)
| Constraint::Store(..)
| Constraint::Lookup(..)
| Constraint::Pattern(..)
| Constraint::True
| Constraint::IsOpenType(_)
| Constraint::IncludesTag(_)
| Constraint::PatternPresence(_, _, _, _)
| Constraint::Exhaustive { .. } => false,
}
}
@ -597,18 +605,41 @@ impl Constraints {
Constraint::Store(type_index, variable, string_index, line_number)
}
pub fn exhaustive(
&mut self,
real_var: Variable,
real_region: Region,
real_category: Category,
expected_branches: Expected<Type>,
sketched_rows: SketchedRows,
context: ExhaustiveContext,
) -> Constraint {
let real_var = Self::push_type_variable(real_var);
let real_category = Index::push_new(&mut self.categories, real_category);
let expected_branches = Index::push_new(&mut self.expectations, expected_branches);
let equality = Eq(real_var, expected_branches, real_category, real_region);
let equality = Index::push_new(&mut self.eq, equality);
let sketched_rows = Index::push_new(&mut self.sketched_rows, sketched_rows);
Constraint::Exhaustive(equality, sketched_rows, context)
}
}
roc_error_macros::assert_sizeof_default!(Constraint, 3 * 8);
roc_error_macros::assert_sizeof_aarch64!(Constraint, 3 * 8);
#[derive(Clone)]
#[derive(Clone, Copy, Debug)]
pub struct Eq(
pub EitherIndex<Type, Variable>,
pub Index<Expected<Type>>,
pub Index<Category>,
pub Region,
);
#[derive(Clone, Copy)]
pub enum Constraint {
Eq(
EitherIndex<Type, Variable>,
Index<Expected<Type>>,
Index<Category>,
Region,
),
Eq(Eq),
Store(
EitherIndex<Type, Variable>,
Variable,
@ -641,6 +672,7 @@ pub enum Constraint {
Index<PatternCategory>,
Region,
),
Exhaustive(Index<Eq>, Index<SketchedRows>, ExhaustiveContext),
}
#[derive(Debug, Clone, Copy, Default)]
@ -670,7 +702,7 @@ pub struct IncludesTag {
impl std::fmt::Debug for Constraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Eq(arg0, arg1, arg2, arg3) => {
Self::Eq(Eq(arg0, arg1, arg2, arg3)) => {
write!(f, "Eq({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3)
}
Self::Store(arg0, arg1, arg2, arg3) => {
@ -695,6 +727,9 @@ impl std::fmt::Debug for Constraint {
arg0, arg1, arg2, arg3
)
}
Self::Exhaustive(arg0, arg1, arg2) => {
write!(f, "Exhaustive({:?}, {:?}, {:?})", arg0, arg1, arg2)
}
}
}
}

View file

@ -1366,6 +1366,7 @@ fn build_effect_loop_inner_body(
region: Region::zero(),
loc_cond: Box::new(force_thunk_call),
branches,
branches_cond_var: var_store.fresh(),
};
Expr::LetNonRec(

View file

@ -0,0 +1,360 @@
use crate::expr::{IntValue, WhenBranch};
use crate::pattern::DestructType;
use roc_collections::all::HumanIndex;
use roc_error_macros::internal_error;
use roc_exhaustive::{
is_useful, Ctor, CtorName, Error, Guard, Literal, Pattern, RenderAs, TagId, Union,
};
use roc_module::ident::{TagIdIntType, TagName};
use roc_region::all::{Loc, Region};
use roc_types::subs::{Content, FlatType, Subs, SubsFmtContent, Variable};
use roc_types::types::AliasKind;
pub use roc_exhaustive::Context as ExhaustiveContext;
pub const GUARD_CTOR: &str = "#Guard";
pub const NONEXHAUSIVE_CTOR: &str = "#Open";
pub fn check(
subs: &Subs,
sketched_rows: SketchedRows,
context: ExhaustiveContext,
) -> Result<(), Vec<Error>> {
let overall_region = sketched_rows.overall_region;
// TODO: can we keep going even if we had redundant rows?
let matrix = sketched_rows
.reify_to_non_redundant(subs)
.map_err(|e| vec![e])?;
roc_exhaustive::check(overall_region, context, matrix)
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum SketchedPattern {
Anything,
Literal(Literal),
Ctor(Variable, TagName, Vec<SketchedPattern>),
KnownCtor(Union, TagId, Vec<SketchedPattern>),
}
impl SketchedPattern {
fn reify(self, subs: &Subs) -> Pattern {
match self {
Self::Anything => Pattern::Anything,
Self::Literal(lit) => Pattern::Literal(lit),
Self::KnownCtor(union, tag_id, patterns) => Pattern::Ctor(
union,
tag_id,
patterns.into_iter().map(|pat| pat.reify(subs)).collect(),
),
Self::Ctor(var, tag_name, patterns) => {
let (union, tag_id) = convert_tag(subs, var, &tag_name);
Pattern::Ctor(
union,
tag_id,
patterns.into_iter().map(|pat| pat.reify(subs)).collect(),
)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SketchedRow {
patterns: Vec<SketchedPattern>,
region: Region,
guard: Guard,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SketchedRows {
rows: Vec<SketchedRow>,
overall_region: Region,
}
impl SketchedRows {
pub fn reify_to_non_redundant(self, subs: &Subs) -> Result<Vec<Vec<Pattern>>, Error> {
to_nonredundant_rows(subs, self)
}
}
fn sketch_pattern(var: Variable, pattern: &crate::pattern::Pattern) -> SketchedPattern {
use crate::pattern::Pattern::*;
use SketchedPattern as SP;
match pattern {
&NumLiteral(_, _, IntValue::I128(n), _) | &IntLiteral(_, _, _, IntValue::I128(n), _) => {
SP::Literal(Literal::Int(n))
}
&NumLiteral(_, _, IntValue::U128(n), _) | &IntLiteral(_, _, _, IntValue::U128(n), _) => {
SP::Literal(Literal::U128(n))
}
&FloatLiteral(_, _, _, f, _) => SP::Literal(Literal::Float(f64::to_bits(f))),
StrLiteral(v) => SP::Literal(Literal::Str(v.clone())),
&SingleQuote(c) => SP::Literal(Literal::Byte(c as u8)),
RecordDestructure { destructs, .. } => {
let tag_id = TagId(0);
let mut patterns = std::vec::Vec::with_capacity(destructs.len());
let mut field_names = std::vec::Vec::with_capacity(destructs.len());
for Loc {
value: destruct,
region: _,
} in destructs
{
field_names.push(destruct.label.clone());
match &destruct.typ {
DestructType::Required | DestructType::Optional(..) => {
patterns.push(SP::Anything)
}
DestructType::Guard(_, guard) => {
patterns.push(sketch_pattern(destruct.var, &guard.value))
}
}
}
let union = Union {
render_as: RenderAs::Record(field_names),
alternatives: vec![Ctor {
name: CtorName::Tag(TagName::Global("#Record".into())),
tag_id,
arity: destructs.len(),
}],
};
SP::KnownCtor(union, tag_id, patterns)
}
AppliedTag {
tag_name,
arguments,
..
} => {
let simplified_args: std::vec::Vec<_> = arguments
.iter()
.map(|(var, arg)| sketch_pattern(*var, &arg.value))
.collect();
SP::Ctor(var, tag_name.clone(), simplified_args)
}
UnwrappedOpaque {
opaque, argument, ..
} => {
let (arg_var, argument) = &(**argument);
let tag_id = TagId(0);
let union = Union {
render_as: RenderAs::Opaque,
alternatives: vec![Ctor {
name: CtorName::Opaque(*opaque),
tag_id,
arity: 1,
}],
};
SP::KnownCtor(
union,
tag_id,
vec![sketch_pattern(*arg_var, &argument.value)],
)
}
// Treat this like a literal so we mark it as non-exhaustive
MalformedPattern(..) => SP::Literal(Literal::Byte(1)),
Underscore
| Identifier(_)
| AbilityMemberSpecialization { .. }
| Shadowed(..)
| OpaqueNotInScope(..)
| UnsupportedPattern(..) => SP::Anything,
}
}
pub fn sketch_rows(target_var: Variable, region: Region, patterns: &[WhenBranch]) -> SketchedRows {
let mut rows: Vec<SketchedRow> = Vec::with_capacity(patterns.len());
// If any of the branches has a guard, e.g.
//
// when x is
// y if y < 10 -> "foo"
// _ -> "bar"
//
// then we treat it as a pattern match on the pattern and a boolean, wrapped in the #Guard
// constructor. We can use this special constructor name to generate better error messages.
// This transformation of the pattern match only works because we only report exhaustiveness
// errors: the Pattern created in this file is not used for code gen.
//
// when x is
// #Guard y True -> "foo"
// #Guard _ _ -> "bar"
let any_has_guard = patterns.iter().any(|branch| branch.guard.is_some());
use SketchedPattern as SP;
for WhenBranch {
patterns,
guard,
value: _,
} in patterns
{
let guard = if guard.is_some() {
Guard::HasGuard
} else {
Guard::NoGuard
};
for loc_pat in patterns {
// Decompose each pattern in the branch into its own row.
let patterns = if any_has_guard {
let guard_pattern = match guard {
Guard::HasGuard => SP::Literal(Literal::Bit(true)),
Guard::NoGuard => SP::Anything,
};
let tag_id = TagId(0);
let union = Union {
render_as: RenderAs::Guard,
alternatives: vec![Ctor {
tag_id,
name: CtorName::Tag(TagName::Global(GUARD_CTOR.into())),
arity: 2,
}],
};
vec![SP::KnownCtor(
union,
tag_id,
// NB: ordering the guard pattern first seems to be better at catching
// non-exhaustive constructors in the second argument; see the paper to see if
// there is a way to improve this in general.
vec![guard_pattern, sketch_pattern(target_var, &loc_pat.value)],
)]
} else {
// Simple case
vec![sketch_pattern(target_var, &loc_pat.value)]
};
let row = SketchedRow {
patterns,
region: loc_pat.region,
guard,
};
rows.push(row);
}
}
SketchedRows {
rows,
overall_region: region,
}
}
/// REDUNDANT PATTERNS
/// INVARIANT: Produces a list of rows where (forall row. length row == 1)
fn to_nonredundant_rows(subs: &Subs, rows: SketchedRows) -> Result<Vec<Vec<Pattern>>, Error> {
let SketchedRows {
rows,
overall_region,
} = rows;
let mut checked_rows = Vec::with_capacity(rows.len());
for SketchedRow {
patterns,
guard,
region,
} in rows.into_iter()
{
let next_row: Vec<Pattern> = patterns
.into_iter()
.map(|pattern| pattern.reify(subs))
.collect();
if matches!(guard, Guard::HasGuard) || is_useful(checked_rows.clone(), next_row.clone()) {
checked_rows.push(next_row);
} else {
return Err(Error::Redundant {
overall_region,
branch_region: region,
index: HumanIndex::zero_based(checked_rows.len()),
});
}
}
Ok(checked_rows)
}
fn convert_tag(subs: &Subs, whole_var: Variable, this_tag: &TagName) -> (Union, TagId) {
let content = subs.get_content_without_compacting(whole_var);
use {Content::*, FlatType::*};
match dealias_tag(subs, content) {
Structure(TagUnion(tags, ext) | RecursiveTagUnion(_, tags, ext)) => {
let (sorted_tags, ext) = tags.sorted_iterator_and_ext(subs, *ext);
let mut num_tags = sorted_tags.len();
// DEVIATION: model openness by attaching a #Open constructor, that can never
// be matched unless there's an `Anything` pattern.
let opt_openness_tag = match subs.get_content_without_compacting(ext) {
FlexVar(_) | RigidVar(_) => {
let openness_tag = TagName::Global(NONEXHAUSIVE_CTOR.into());
num_tags += 1;
Some((openness_tag, &[] as _))
}
Structure(EmptyTagUnion) => None,
// Anything else is erroneous and we ignore
_ => None,
};
// High tag ID if we're out-of-bounds.
let mut my_tag_id = TagId(num_tags as TagIdIntType);
let mut alternatives = Vec::with_capacity(num_tags);
let alternatives_iter = sorted_tags.into_iter().chain(opt_openness_tag.into_iter());
for (index, (tag, args)) in alternatives_iter.enumerate() {
let tag_id = TagId(index as TagIdIntType);
if this_tag == &tag {
my_tag_id = tag_id;
}
alternatives.push(Ctor {
name: CtorName::Tag(tag),
tag_id,
arity: args.len(),
});
}
let union = Union {
alternatives,
render_as: RenderAs::Tag,
};
(union, my_tag_id)
}
_ => internal_error!(
"Content is not a tag union: {:?}",
SubsFmtContent(content, subs)
),
}
}
pub fn dealias_tag<'a>(subs: &'a Subs, content: &'a Content) -> &'a Content {
use Content::*;
let mut result = content;
loop {
match result {
Alias(_, _, real_var, AliasKind::Structural)
| RecursionVar {
structure: real_var,
..
} => result = subs.get_content_without_compacting(*real_var),
_ => return result,
}
}
}

View file

@ -19,7 +19,7 @@ use roc_parse::pattern::PatternType::*;
use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::subs::{VarStore, Variable};
use roc_types::types::{Alias, LambdaSet, Type};
use roc_types::types::{Alias, Category, LambdaSet, Type};
use std::fmt::{Debug, Display};
use std::{char, u32};
@ -89,6 +89,7 @@ pub enum Expr {
region: Region,
loc_cond: Box<Loc<Expr>>,
branches: Vec<WhenBranch>,
branches_cond_var: Variable,
},
If {
cond_var: Variable,
@ -193,6 +194,47 @@ pub enum Expr {
// Compiles, but will crash if reached
RuntimeError(RuntimeError),
}
impl Expr {
pub fn category(&self) -> Category {
match self {
Self::Num(..) => Category::Num,
Self::Int(..) => Category::Int,
Self::Float(..) => Category::Float,
Self::Str(..) => Category::Str,
Self::SingleQuote(..) => Category::Character,
Self::List { .. } => Category::List,
&Self::Var(sym) => Category::Lookup(sym),
Self::When { .. } => Category::When,
Self::If { .. } => Category::If,
Self::LetRec(_, expr, _) => expr.value.category(),
Self::LetNonRec(_, expr, _) => expr.value.category(),
&Self::Call(_, _, called_via) => Category::CallResult(None, called_via),
&Self::RunLowLevel { op, .. } => Category::LowLevelOpResult(op),
Self::ForeignCall { .. } => Category::ForeignCall,
Self::Closure(..) => Category::Lambda,
Self::Record { .. } => Category::Record,
Self::EmptyRecord => Category::Record,
Self::Access { field, .. } => Category::Access(field.clone()),
Self::Accessor(data) => Category::Accessor(data.field.clone()),
Self::Update { .. } => Category::Record,
Self::Tag {
name, arguments, ..
} => Category::TagApply {
tag_name: name.clone(),
args_count: arguments.len(),
},
Self::ZeroArgumentTag { name, .. } => Category::TagApply {
tag_name: name.clone(),
args_count: 0,
},
&Self::OpaqueRef { name, .. } => Category::OpaqueWrap(name),
Self::Expect(..) => Category::Expect,
Self::RuntimeError(..) => Category::Unknown,
}
}
}
#[derive(Clone, Debug)]
pub struct ClosureData {
pub function_type: Variable,
@ -782,6 +824,7 @@ pub fn canonicalize_expr<'a>(
region,
loc_cond: Box::new(can_cond),
branches: can_branches,
branches_cond_var: var_store.fresh(),
};
(expr, output)
@ -1298,6 +1341,7 @@ pub fn inline_calls(var_store: &mut VarStore, scope: &mut Scope, expr: Expr) ->
region,
loc_cond,
branches,
branches_cond_var,
} => {
let loc_cond = Box::new(Loc {
region: loc_cond.region,
@ -1333,6 +1377,7 @@ pub fn inline_calls(var_store: &mut VarStore, scope: &mut Scope, expr: Expr) ->
region,
loc_cond,
branches: new_branches,
branches_cond_var,
}
}
If {

View file

@ -8,6 +8,7 @@ pub mod constraint;
pub mod def;
pub mod effect_module;
pub mod env;
pub mod exhaustive;
pub mod expected;
pub mod expr;
pub mod module;

View file

@ -65,6 +65,7 @@ fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr) {
loc_cond,
branches,
region: _,
branches_cond_var: _,
} => {
walk_when(visitor, *cond_var, *expr_var, loc_cond, branches);
}