Merge pull request #4076 from roc-lang/collapse-void-2

Unwrap layouts containing void layouts as newtypes
This commit is contained in:
Folkert de Vries 2022-09-21 00:13:56 +02:00 committed by GitHub
commit a74f5d9366
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 330 additions and 38 deletions

1
Cargo.lock generated
View file

@ -3895,6 +3895,7 @@ dependencies = [
name = "roc_mono"
version = "0.0.1"
dependencies = [
"bitvec 1.0.1",
"bumpalo",
"hashbrown 0.12.3",
"roc_builtins",

View file

@ -393,11 +393,11 @@ contains = \list, needle ->
## `fold`, `foldLeft`, or `foldl`.
walk : List elem, state, (state, elem -> state) -> state
walk = \list, state, func ->
walkHelp : _, _ -> [Continue _, Break []]
walkHelp = \currentState, element -> Continue (func currentState element)
when List.iterate list state walkHelp is
Continue newState -> newState
Break void -> List.unreachable void
## Note that in other languages, `walkBackwards` is sometimes called `reduceRight`,
## `fold`, `foldRight`, or `foldr`.
@ -1006,6 +1006,3 @@ iterBackwardsHelp = \list, state, f, prevIndex ->
Break b -> Break b
else
Continue state
## useful for typechecking guaranteed-unreachable cases
unreachable : [] -> a

View file

@ -27,3 +27,4 @@ ven_pretty = { path = "../../vendor/pretty" }
bumpalo = { version = "3.11.0", features = ["collections"] }
hashbrown = { version = "0.12.3", features = [ "bumpalo" ] }
static_assertions = "1.1.0"
bitvec = "1.0.1"

View file

@ -4,6 +4,7 @@ use crate::ir::{
use crate::layout::{Builtin, Layout, LayoutCache, TagIdIntType, UnionLayout};
use roc_builtins::bitcode::{FloatWidth, IntWidth};
use roc_collections::all::{MutMap, MutSet};
use roc_error_macros::internal_error;
use roc_exhaustive::{Ctor, CtorName, RenderAs, TagId, Union};
use roc_module::ident::TagName;
use roc_module::low_level::LowLevel;
@ -577,6 +578,8 @@ fn test_at_path<'a>(
arguments: arguments.to_vec(),
},
Voided { .. } => internal_error!("unreachable"),
OpaqueUnwrap { opaque, argument } => {
let union = Union {
render_as: RenderAs::Tag,
@ -875,6 +878,7 @@ fn to_relevant_branch_help<'a>(
_ => None,
}
}
Voided { .. } => internal_error!("unreachable"),
StrLiteral(string) => match test {
IsStr(test_str) if string == *test_str => {
start.extend(end);
@ -1018,6 +1022,8 @@ fn needs_tests(pattern: &Pattern) -> bool {
| FloatLiteral(_, _)
| DecimalLiteral(_)
| StrLiteral(_) => true,
Voided { .. } => internal_error!("unreachable"),
}
}

View file

@ -5731,7 +5731,7 @@ fn convert_tag_union<'a>(
"The `[]` type has no constructors, source var {:?}",
variant_var
),
Unit | UnitWithArguments => Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole),
Unit => Stmt::Let(assigned, Expr::Struct(&[]), Layout::UNIT, hole),
BoolUnion { ttrue, .. } => Stmt::Let(
assigned,
Expr::Literal(Literal::Bool(&tag_name == ttrue.expect_tag_ref())),
@ -5781,6 +5781,41 @@ fn convert_tag_union<'a>(
let iter = field_symbols_temp.into_iter().map(|(_, _, data)| data);
assign_to_symbols(env, procs, layout_cache, iter, stmt)
}
NewtypeByVoid {
data_tag_arguments: field_layouts,
data_tag_name,
..
} => {
let dataful_tag = data_tag_name.expect_tag();
if dataful_tag != tag_name {
// this tag is not represented, and hence will never be reached, at runtime.
Stmt::RuntimeError("voided tag constructor is unreachable")
} else {
let field_symbols_temp = sorted_field_symbols(env, procs, layout_cache, args);
let mut field_symbols = Vec::with_capacity_in(field_layouts.len(), env.arena);
field_symbols.extend(field_symbols_temp.iter().map(|r| r.1));
let field_symbols = field_symbols.into_bump_slice();
// Layout will unpack this unwrapped tack if it only has one (non-zero-sized) field
let layout = layout_cache
.from_var(env.arena, variant_var, env.subs)
.unwrap_or_else(|err| panic!("TODO turn fn_var into a RuntimeError {:?}", err));
// even though this was originally a Tag, we treat it as a Struct from now on
let stmt = if let [only_field] = field_symbols {
let mut hole = hole.clone();
substitute_in_exprs(env.arena, &mut hole, assigned, *only_field);
hole
} else {
Stmt::Let(assigned, Expr::Struct(field_symbols), layout, hole)
};
let iter = field_symbols_temp.into_iter().map(|(_, _, data)| data);
assign_to_symbols(env, procs, layout_cache, iter, stmt)
}
}
Wrapped(variant) => {
let (tag_id, _) = variant.tag_name_to_id(&tag_name);
@ -6520,7 +6555,23 @@ fn from_can_when<'a>(
let arena = env.arena;
let it = opt_branches
.into_iter()
.map(|(pattern, opt_guard, can_expr)| {
.filter_map(|(pattern, opt_guard, can_expr)| {
// If the pattern has a void layout we can drop it; however, we must still perform the
// work of building the body, because that may contain specializations we must
// discover for use elsewhere. See
// `unreachable_branch_is_eliminated_but_produces_lambda_specializations` in test_mono
// for an example.
let should_eliminate_branch = pattern.is_voided();
// If we're going to eliminate the branch, we need to take a snapshot of the symbol
// specializations before we enter the branch, because any new specializations that
// will be added in the branch body will never need to be resolved!
let specialization_symbol_snapshot = if should_eliminate_branch {
Some(std::mem::take(&mut procs.symbol_specializations))
} else {
None
};
let branch_stmt = match join_point {
None => from_can(env, expr_var, can_expr, procs, layout_cache),
Some(id) => {
@ -6533,7 +6584,7 @@ fn from_can_when<'a>(
};
use crate::decision_tree::Guard;
if let Some(loc_expr) = opt_guard {
let result = if let Some(loc_expr) = opt_guard {
let id = JoinPointId(env.unique_symbol());
let symbol = env.unique_symbol();
let jump = env.arena.alloc(Stmt::Jump(id, env.arena.alloc([symbol])));
@ -6559,6 +6610,13 @@ fn from_can_when<'a>(
)
} else {
(pattern, Guard::NoGuard, branch_stmt)
};
if should_eliminate_branch {
procs.symbol_specializations = specialization_symbol_snapshot.unwrap();
None
} else {
Some(result)
}
});
let mono_branches = Vec::from_iter_in(it, arena);
@ -7085,6 +7143,10 @@ fn store_pattern_help<'a>(
stmt,
);
}
Voided { .. } => {
return StorePattern::NotProductive(stmt);
}
OpaqueUnwrap { argument, .. } => {
let (pattern, _layout) = &**argument;
return store_pattern_help(env, procs, layout_cache, pattern, outer_symbol, stmt);
@ -8676,12 +8738,56 @@ pub enum Pattern<'a> {
layout: UnionLayout<'a>,
union: roc_exhaustive::Union,
},
Voided {
tag_name: TagName,
},
OpaqueUnwrap {
opaque: Symbol,
argument: Box<(Pattern<'a>, Layout<'a>)>,
},
}
impl<'a> Pattern<'a> {
/// This pattern contains a pattern match on Void (i.e. [], the empty tag union)
/// such branches are not reachable at runtime
pub fn is_voided(&self) -> bool {
let mut stack: std::vec::Vec<&Pattern> = vec![self];
while let Some(pattern) = stack.pop() {
match pattern {
Pattern::Identifier(_)
| Pattern::Underscore
| Pattern::IntLiteral(_, _)
| Pattern::FloatLiteral(_, _)
| Pattern::DecimalLiteral(_)
| Pattern::BitLiteral { .. }
| Pattern::EnumLiteral { .. }
| Pattern::StrLiteral(_) => { /* terminal */ }
Pattern::RecordDestructure(destructs, _) => {
for destruct in destructs {
match &destruct.typ {
DestructType::Required(_) => { /* do nothing */ }
DestructType::Guard(pattern) => {
stack.push(pattern);
}
}
}
}
Pattern::NewtypeDestructure { arguments, .. } => {
stack.extend(arguments.iter().map(|(t, _)| t))
}
Pattern::Voided { .. } => return true,
Pattern::AppliedTag { arguments, .. } => {
stack.extend(arguments.iter().map(|(t, _)| t))
}
Pattern::OpaqueUnwrap { argument, .. } => stack.push(&argument.0),
}
}
false
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RecordDestruct<'a> {
pub label: Lowercase,
@ -8808,7 +8914,7 @@ fn from_can_pattern_help<'a>(
"there is no pattern of type `[]`, union var {:?}",
*whole_var
),
Unit | UnitWithArguments => Pattern::EnumLiteral {
Unit => Pattern::EnumLiteral {
tag_id: 0,
tag_name: tag_name.clone(),
union: Union {
@ -8907,6 +9013,57 @@ fn from_can_pattern_help<'a>(
arguments: mono_args,
}
}
NewtypeByVoid {
data_tag_arguments,
data_tag_name,
..
} => {
let data_tag_name = data_tag_name.expect_tag();
if tag_name != &data_tag_name {
// this tag is not represented at runtime
Pattern::Voided {
tag_name: tag_name.clone(),
}
} else {
let mut arguments = arguments.clone();
arguments.sort_by(|arg1, arg2| {
let size1 = layout_cache
.from_var(env.arena, arg1.0, env.subs)
.map(|x| x.alignment_bytes(&layout_cache.interner, env.target_info))
.unwrap_or(0);
let size2 = layout_cache
.from_var(env.arena, arg2.0, env.subs)
.map(|x| x.alignment_bytes(&layout_cache.interner, env.target_info))
.unwrap_or(0);
size2.cmp(&size1)
});
let mut mono_args = Vec::with_capacity_in(arguments.len(), env.arena);
let it = arguments.iter().zip(data_tag_arguments.iter());
for ((_, loc_pat), layout) in it {
mono_args.push((
from_can_pattern_help(
env,
procs,
layout_cache,
&loc_pat.value,
assignments,
)?,
*layout,
));
}
Pattern::NewtypeDestructure {
tag_name: tag_name.clone(),
arguments: mono_args,
}
}
}
Wrapped(variant) => {
let (tag_id, argument_layouts) = variant.tag_name_to_id(tag_name);
let number_of_tags = variant.number_of_tags();

View file

@ -1,4 +1,5 @@
use crate::ir::Parens;
use bitvec::vec::BitVec;
use bumpalo::collections::Vec;
use bumpalo::Bump;
use roc_builtins::bitcode::{FloatWidth, IntWidth};
@ -3270,7 +3271,6 @@ impl From<Symbol> for TagOrClosure {
pub enum UnionVariant<'a> {
Never,
Unit,
UnitWithArguments,
BoolUnion {
ttrue: TagOrClosure,
ffalse: TagOrClosure,
@ -3280,6 +3280,11 @@ pub enum UnionVariant<'a> {
tag_name: TagOrClosure,
arguments: Vec<'a, Layout<'a>>,
},
NewtypeByVoid {
data_tag_name: TagOrClosure,
data_tag_id: TagIdIntType,
data_tag_arguments: Vec<'a, Layout<'a>>,
},
Wrapped(WrappedVariant<'a>),
}
@ -3526,6 +3531,8 @@ where
Vec::with_capacity_in(tags_list.len(), env.arena);
let mut has_any_arguments = false;
let mut inhabited_tag_ids = BitVec::<usize>::repeat(true, num_tags);
for &(tag_name, arguments) in tags_list.into_iter() {
let mut arg_layouts = Vec::with_capacity_in(arguments.len() + 1, env.arena);
@ -3537,6 +3544,10 @@ where
has_any_arguments = true;
arg_layouts.push(layout);
if layout == Layout::VOID {
inhabited_tag_ids.set(answer.len(), false);
}
}
Err(LayoutProblem::UnresolvedTypeVar(_)) => {
// If we encounter an unbound type var (e.g. `Ok *`)
@ -3561,6 +3572,18 @@ where
answer.push((tag_name.clone().into(), arg_layouts.into_bump_slice()));
}
if inhabited_tag_ids.count_ones() == 1 {
let kept_tag_id = inhabited_tag_ids.first_one().unwrap();
let kept = answer.get(kept_tag_id).unwrap();
let variant = UnionVariant::NewtypeByVoid {
data_tag_name: kept.0.clone(),
data_tag_id: kept_tag_id as _,
data_tag_arguments: Vec::from_iter_in(kept.1.iter().copied(), env.arena),
};
return Cacheable(variant, cache_criteria);
}
match num_tags {
2 if !has_any_arguments => {
// type can be stored in a boolean
@ -3628,19 +3651,13 @@ where
// just one tag in the union (but with arguments) can be a struct
let mut layouts = Vec::with_capacity_in(tags_vec.len(), env.arena);
let mut contains_zero_sized = false;
for var in arguments {
let Cacheable(result, criteria) = Layout::from_var(env, var);
cache_criteria.and(criteria);
match result {
Ok(layout) => {
// Drop any zero-sized arguments like {}
if !layout.is_dropped_because_empty() {
layouts.push(layout);
} else {
contains_zero_sized = true;
}
layouts.push(layout);
}
Err(LayoutProblem::UnresolvedTypeVar(_)) => {
// If we encounter an unbound type var (e.g. `Ok *`)
@ -3663,11 +3680,7 @@ where
});
if layouts.is_empty() {
if contains_zero_sized {
Cacheable(UnionVariant::UnitWithArguments, cache_criteria)
} else {
Cacheable(UnionVariant::Unit, cache_criteria)
}
Cacheable(UnionVariant::Unit, cache_criteria)
} else if let Some(rec_var) = opt_rec_var {
let variant = UnionVariant::Wrapped(WrappedVariant::NonNullableUnwrapped {
tag_name: tag_name.into(),
@ -3691,6 +3704,7 @@ where
let mut has_any_arguments = false;
let mut nullable = None;
let mut inhabited_tag_ids = BitVec::<usize>::repeat(true, num_tags);
// only recursive tag unions can be nullable
let is_recursive = opt_rec_var.is_some();
@ -3732,6 +3746,10 @@ where
} else {
arg_layouts.push(layout);
}
if layout == Layout::VOID {
inhabited_tag_ids.set(answer.len(), false);
}
}
Err(LayoutProblem::UnresolvedTypeVar(_)) => {
// If we encounter an unbound type var (e.g. `Ok *`)
@ -3757,6 +3775,18 @@ where
answer.push((tag_name.into(), arg_layouts.into_bump_slice()));
}
if inhabited_tag_ids.count_ones() == 1 && !is_recursive {
let kept_tag_id = inhabited_tag_ids.first_one().unwrap();
let kept = answer.get(kept_tag_id).unwrap();
let variant = UnionVariant::NewtypeByVoid {
data_tag_name: kept.0.clone(),
data_tag_id: kept_tag_id as _,
data_tag_arguments: Vec::from_iter_in(kept.1.iter().copied(), env.arena),
};
return Cacheable(variant, cache_criteria);
}
match num_tags {
2 if !has_any_arguments => {
// type can be stored in a boolean
@ -3866,7 +3896,7 @@ where
let result = match variant {
Never => Layout::VOID,
Unit | UnitWithArguments => Layout::UNIT,
Unit => Layout::UNIT,
BoolUnion { .. } => Layout::bool(),
ByteUnion(_) => Layout::u8(),
Newtype {
@ -3881,6 +3911,15 @@ where
answer1
}
NewtypeByVoid {
data_tag_arguments, ..
} => {
if data_tag_arguments.len() == 1 {
data_tag_arguments[0]
} else {
Layout::struct_no_name_order(data_tag_arguments.into_bump_slice())
}
}
Wrapped(variant) => {
use WrappedVariant::*;

View file

@ -262,8 +262,9 @@ fn list_map_try_ok() {
r#"
List.mapTry [1, 2, 3] \elem -> Ok elem
"#,
RocResult::ok(RocList::<i64>::from_slice(&[1, 2, 3])),
RocResult<RocList<i64>, ()>
// Result I64 [] is unwrapped to just I64
RocList::<i64>::from_slice(&[1, 2, 3]),
RocList<i64>
);
assert_evals_to!(
// Transformation
@ -273,12 +274,13 @@ fn list_map_try_ok() {
Ok "\(str)!"
"#,
RocResult::ok(RocList::<RocStr>::from_slice(&[
// Result Str [] is unwrapped to just Str
RocList::<RocStr>::from_slice(&[
RocStr::from("2!"),
RocStr::from("4!"),
RocStr::from("6!"),
])),
RocResult<RocList<RocStr>, ()>
]),
RocList<RocStr>
);
}
@ -3004,8 +3006,10 @@ fn list_find_empty_layout() {
List.findFirst [] \_ -> True
"#
),
RocResult::err(()),
RocResult<(), ()>
// [Ok [], Err [NotFound]] gets unwrapped all the way to just [NotFound],
// which is the unit!
(),
()
);
assert_evals_to!(
@ -3014,8 +3018,10 @@ fn list_find_empty_layout() {
List.findLast [] \_ -> True
"#
),
RocResult::err(()),
RocResult<(), ()>
// [Ok [], Err [NotFound]] gets unwrapped all the way to just [NotFound],
// which is the unit!
(),
()
);
}

View file

@ -1,7 +1,3 @@
procedure Test.0 ():
let Test.5 : Str = "abc";
let Test.1 : [C [], C Str] = TagId(1) Test.5;
let Test.3 : Str = UnionAtIndex (Id 1) (Index 0) Test.1;
inc Test.3;
dec Test.1;
ret Test.3;
ret Test.5;

View file

@ -0,0 +1,29 @@
procedure Test.1 (Test.2):
let Test.3 : Int1 = false;
ret Test.3;
procedure Test.3 (Test.13):
let Test.15 : Str = "t1";
ret Test.15;
procedure Test.4 (Test.16):
let Test.18 : Str = "t2";
ret Test.18;
procedure Test.0 ():
let Test.19 : Str = "abc";
let Test.6 : Int1 = CallByName Test.1 Test.19;
dec Test.19;
let Test.9 : {} = Struct {};
joinpoint Test.10 Test.8:
ret Test.8;
in
switch Test.6:
case 0:
let Test.11 : Str = CallByName Test.3 Test.9;
jump Test.10 Test.11;
default:
let Test.12 : Str = CallByName Test.4 Test.9;
jump Test.10 Test.12;

View file

@ -0,0 +1,7 @@
procedure Test.0 ():
let Test.7 : Int1 = true;
if Test.7 then
Error voided tag constructor is unreachable
else
let Test.6 : Str = "abc";
ret Test.6;

View file

@ -1949,3 +1949,54 @@ fn match_on_result_with_uninhabited_error_branch() {
"#
)
}
#[mono_test]
fn unreachable_void_constructor() {
indoc!(
r#"
app "test" provides [main] to "./platform"
x : []
main = if True then Ok x else Err "abc"
"#
)
}
#[mono_test]
fn unreachable_branch_is_eliminated_but_produces_lambda_specializations() {
indoc!(
r#"
app "test" provides [main] to "./platform"
provideThunk = \x ->
when x is
Ok _ ->
t1 = \{} -> "t1"
t1
# During specialization of `main` we specialize this function,
# which leads to elimination of this branch, because it is unreachable
# (it can only match the uninhabited type `Err []`).
#
# However, naive elimination of this branch would mean we don't traverse
# the branch body. If we don't do so, we will fail to see and specialize `t2`,
# which is problematic - while `t2` won't ever be reached in this specialization,
# it is still part of the lambda set, and `thunk {}` (in main) will match over
# it before calling.
#
# So, this test verifies that we eliminate this branch, but still specialize
# everything we need.
Err _ ->
t2 = \{} -> "t2"
t2
main =
x : Result Str []
x = Ok "abc"
thunk = provideThunk x
thunk {}
"#
)
}

View file

@ -88,7 +88,8 @@ export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void {
const Unit = extern struct {};
pub export fn main() callconv(.C) u8 {
const size = @intCast(usize, roc__mainForHost_size());
// The size might be zero; if so, make it at least 8 so that we don't have a nullptr
const size = std.math.max(@intCast(usize, roc__mainForHost_size()), 8);
const raw_output = roc_alloc(@intCast(usize, size), @alignOf(u64)).?;
var output = @ptrCast([*]u8, raw_output);
@ -120,7 +121,8 @@ fn to_seconds(tms: std.os.timespec) f64 {
fn call_the_closure(closure_data_pointer: [*]u8) void {
const allocator = std.heap.page_allocator;
const size = roc__mainForHost_1__Fx_result_size();
// The size might be zero; if so, make it at least 8 so that we don't have a nullptr
const size = std.math.max(roc__mainForHost_1__Fx_result_size(), 8);
const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable;
var output = @ptrCast([*]u8, raw_output);