roc/compiler/test_mono/src/tests.rs
ayazhafiz e52d427ac8 Hash record field name order in generated layouts
Closes #2535

See the referenced issue for longer discussion - here's the synopsis.
Consider this program

```
app "test" provides [ nums ] to "./platform"

alpha = { a: 1, b: 2 }

nums : List U8
nums =
    [
        alpha.a,
        alpha.b,
    ]
```

Here's its IR:

```
procedure : `#UserApp.alpha` {I64, U8}
procedure = `#UserApp.alpha` ():
    let `#UserApp.5` : Builtin(Int(I64)) = 1i64;
    let `#UserApp.6` : Builtin(Int(U8)) = 2i64;
    let `#UserApp.4` : Struct([Builtin(Int(I64)), Builtin(Int(U8))]) = Struct {`#UserApp.5`, `#UserApp.6`};
    ret `#UserApp.4`;

procedure : `#UserApp.nums` List U8
procedure = `#UserApp.nums` ():
    let `#UserApp.7` : Struct([Builtin(Int(I64)), Builtin(Int(U8))]) = CallByName `#UserApp.alpha`;
    let `#UserApp.1` : Builtin(Int(U8)) = StructAtIndex 1 `#UserApp.7`;
    let `#UserApp.3` : Struct([Builtin(Int(I64)), Builtin(Int(U8))]) = CallByName `#UserApp.alpha`;
    let `#UserApp.2` : Builtin(Int(U8)) = StructAtIndex 1 `#UserApp.3`;
    let `#UserApp.0` : Builtin(List(Builtin(Int(U8)))) = Array [`#UserApp.1`, `#UserApp.2`];
    ret `#UserApp.0`;
```

What's happening is that we need to specialize `alpha` twice - once for the
type of a narrowed to a U8, another time for the type of b narrowed to a U8.

We do the specialization for alpha.b first - record fields are sorted by
layout, so we generate a record of type {i64, u8}. But then we go to
specialize alpha.a, but this has the same layout - {i64, u8} - so we reuse
the existing one! So (at least for records), we need to include record field
order associated with the sorted layout fields, so that we don't reuse
monomorphizations like this incorrectly!
2022-02-21 14:10:45 -05:00

1287 lines
23 KiB
Rust

#![cfg(test)]
#![warn(clippy::dbg_macro)]
// See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check.
#![allow(clippy::large_enum_variant)]
// we actually want to compare against the literal float bits
#![allow(clippy::float_cmp)]
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate indoc;
/// Used in the with_larger_debug_stack() function, for tests that otherwise
/// run out of stack space in debug builds (but don't in --release builds)
#[allow(dead_code)]
const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024;
use test_mono_macros::*;
use roc_collections::all::MutMap;
use roc_module::symbol::Symbol;
use roc_mono::ir::Proc;
use roc_mono::ir::ProcLayout;
const TARGET_INFO: roc_target::TargetInfo = roc_target::TargetInfo::default_x86_64();
/// Without this, some tests pass in `cargo test --release` but fail without
/// the --release flag because they run out of stack space. This increases
/// stack size for debug builds only, while leaving the stack space at the default
/// amount for release builds.
#[allow(dead_code)]
#[cfg(debug_assertions)]
pub fn with_larger_debug_stack<F>(run_test: F)
where
F: FnOnce(),
F: Send,
F: 'static,
{
std::thread::Builder::new()
.stack_size(EXPANDED_STACK_SIZE)
.spawn(run_test)
.expect("Error while spawning expanded dev stack size thread")
.join()
.expect("Error while joining expanded dev stack size thread")
}
/// In --release builds, don't increase the stack size. Run the test normally.
/// This way, we find out if any of our tests are blowing the stack even after
/// optimizations in release builds.
#[allow(dead_code)]
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn with_larger_debug_stack<F>(run_test: F)
where
F: FnOnce() -> (),
F: Send,
F: 'static,
{
run_test()
}
fn promote_expr_to_module(src: &str) -> String {
let mut buffer = String::from("app \"test\" provides [ main ] to \"./platform\"\n\nmain =\n");
for line in src.lines() {
// indent the body!
buffer.push_str(" ");
buffer.push_str(line);
buffer.push('\n');
}
buffer
}
fn compiles_to_ir(test_name: &str, src: &str) {
use bumpalo::Bump;
use std::path::{Path, PathBuf};
let arena = &Bump::new();
// let stdlib = roc_builtins::unique::uniq_stdlib();
let stdlib = roc_builtins::std::standard_stdlib();
let filename = PathBuf::from("Test.roc");
let src_dir = Path::new("fake/test/path");
let module_src;
let temp;
if src.starts_with("app") {
// this is already a module
module_src = src;
} else {
// this is an expression, promote it to a module
temp = promote_expr_to_module(src);
module_src = &temp;
}
let exposed_types = MutMap::default();
let loaded = roc_load::file::load_and_monomorphize_from_str(
arena,
filename,
module_src,
&stdlib,
src_dir,
exposed_types,
TARGET_INFO,
);
let mut loaded = match loaded {
Ok(x) => x,
Err(roc_load::file::LoadingProblem::FormattedReport(report)) => {
println!("{}", report);
panic!();
}
Err(e) => panic!("{:?}", e),
};
use roc_load::file::MonomorphizedModule;
let MonomorphizedModule {
module_id: home,
procedures,
exposed_to_host,
..
} = loaded;
let can_problems = loaded.can_problems.remove(&home).unwrap_or_default();
let type_problems = loaded.type_problems.remove(&home).unwrap_or_default();
let mono_problems = loaded.mono_problems.remove(&home).unwrap_or_default();
if !can_problems.is_empty() {
println!("Ignoring {} canonicalization problems", can_problems.len());
}
assert_eq!(type_problems, Vec::new());
assert_eq!(mono_problems, Vec::new());
debug_assert_eq!(exposed_to_host.values.len(), 1);
let main_fn_symbol = exposed_to_host.values.keys().copied().next().unwrap();
verify_procedures(test_name, procedures, main_fn_symbol);
}
#[cfg(debug_assertions)]
fn verify_procedures(
test_name: &str,
procedures: MutMap<(Symbol, ProcLayout<'_>), Proc<'_>>,
main_fn_symbol: Symbol,
) {
let index = procedures
.keys()
.position(|(s, _)| *s == main_fn_symbol)
.unwrap();
let mut procs_string = procedures
.values()
.map(|proc| proc.to_pretty(200))
.collect::<Vec<_>>();
let main_fn = procs_string.swap_remove(index);
procs_string.sort();
procs_string.push(main_fn);
let result = procs_string.join("\n");
let path = format!("generated/{}.txt", test_name);
std::fs::create_dir_all("generated").unwrap();
std::fs::write(&path, result).unwrap();
use std::process::Command;
let is_tracked = Command::new("git")
.args(&["ls-files", "--error-unmatch", &path])
.output()
.unwrap();
if !is_tracked.status.success() {
panic!(
"The file {:?} is not tracked by git. Try using `git add` on it",
&path
);
}
let has_changes = Command::new("git")
.args(&["diff", "--color=always", &path])
.output()
.unwrap();
if !has_changes.status.success() {
eprintln!("`git diff {:?}` failed", &path);
unreachable!();
}
if !has_changes.stdout.is_empty() {
println!("{}", std::str::from_utf8(&has_changes.stdout).unwrap());
panic!("Output changed: resolve conflicts and `git add` the file.");
}
}
// NOTE because the Show instance of module names is different in --release mode,
// these tests would all fail. In the future, when we do interesting optimizations,
// we'll likely want some tests for --release too.
#[cfg(not(debug_assertions))]
fn verify_procedures(
_expected: &str,
_procedures: MutMap<(Symbol, ProcLayout<'_>), Proc<'_>>,
_main_fn_symbol: Symbol,
) {
// Do nothing
}
#[mono_test]
fn ir_int_literal() {
r#"
5
"#
}
#[mono_test]
fn ir_int_add() {
r#"
x = [ 1,2 ]
5 + 4 + 3 + List.len x
"#
}
#[mono_test]
fn ir_assignment() {
r#"
x = 5
x
"#
}
#[mono_test]
fn ir_when_maybe() {
r#"
when Just 3 is
Just n -> n
Nothing -> 0
"#
}
#[mono_test]
fn ir_when_these() {
r#"
when These 1 2 is
This x -> x
That y -> y
These x _ -> x
"#
}
#[mono_test]
fn ir_when_record() {
r#"
when { x: 1, y: 3.14 } is
{ x } -> x
"#
}
#[mono_test]
fn ir_plus() {
r#"
1 + 2
"#
}
#[mono_test]
fn ir_round() {
r#"
Num.round 3.6
"#
}
#[mono_test]
fn ir_when_idiv() {
r#"
when 1000 // 10 is
Ok val -> val
Err _ -> -1
"#
}
#[mono_test]
fn ir_two_defs() {
r#"
x = 3
y = 4
x + y
"#
}
#[mono_test]
fn ir_when_just() {
r#"
x : [ Nothing, Just I64 ]
x = Just 41
when x is
Just v -> v + 0x1
Nothing -> 0x1
"#
}
#[mono_test]
fn one_element_tag() {
r#"
x : [ Pair I64 ]
x = Pair 2
x
"#
}
#[mono_test]
fn guard_pattern_true() {
r#"
wrapper = \{} ->
when 2 is
2 if False -> 42
_ -> 0
wrapper {}
"#
}
#[mono_test]
fn when_on_record() {
r#"
when { x: 0x2 } is
{ x } -> x + 3
"#
}
#[mono_test]
fn when_nested_maybe() {
r#"
Maybe a : [ Nothing, Just a ]
x : Maybe (Maybe I64)
x = Just (Just 41)
when x is
Just (Just v) -> v + 0x1
_ -> 0x1
"#
}
#[mono_test]
fn when_on_two_values() {
r#"
when Pair 2 3 is
Pair 4 3 -> 9
Pair a b -> a + b
"#
}
#[mono_test]
fn dict() {
r#"
Dict.len Dict.empty
"#
}
#[mono_test]
fn list_append_closure() {
r#"
myFunction = \l -> List.append l 42
myFunction [ 1, 2 ]
"#
}
#[mono_test]
fn list_append() {
// TODO this leaks at the moment
// ListAppend needs to decrement its arguments
r#"
List.append [1] 2
"#
}
#[mono_test]
fn list_len() {
r#"
x = [1,2,3]
y = [ 1.0 ]
List.len x + List.len y
"#
}
#[mono_test]
fn when_joinpoint() {
r#"
wrapper = \{} ->
x : [ Red, White, Blue ]
x = Blue
y =
when x is
Red -> 1
White -> 2
Blue -> 3
y
wrapper {}
"#
}
#[mono_test]
fn simple_if() {
r#"
if True then
1
else
2
"#
}
#[mono_test]
fn if_multi_branch() {
r#"
if True then
1
else if False then
2
else
3
"#
}
#[mono_test]
fn when_on_result() {
r#"
wrapper = \{} ->
x : Result I64 I64
x = Ok 2
y =
when x is
Ok 3 -> 1
Ok _ -> 2
Err _ -> 3
y
wrapper {}
"#
}
#[mono_test]
fn let_with_record_pattern() {
r#"
{ x } = { x: 0x2, y: 3.14 }
x
"#
}
#[mono_test]
fn let_with_record_pattern_list() {
r#"
{ x } = { x: [ 1, 3, 4 ], y: 3.14 }
x
"#
}
#[mono_test]
fn if_guard_bind_variable_false() {
r#"
wrapper = \{} ->
when 10 is
x if x == 5 -> 0
_ -> 42
wrapper {}
"#
}
#[mono_test]
fn alias_variable() {
r#"
x = 5
y = x
3
"#
}
#[mono_test]
fn alias_variable_and_return_it() {
r#"
x = 5
y = x
y
"#
}
#[mono_test]
fn branch_store_variable() {
r#"
when 0 is
1 -> 12
a -> a
"#
}
#[mono_test]
fn list_pass_to_function() {
r#"
x : List I64
x = [1,2,3]
id : List I64 -> List I64
id = \y -> List.set y 0 0
id x
"#
}
#[mono_test]
fn record_optional_field_let_no_use_default() {
r#"
f = \r ->
{ x ? 10, y } = r
x + y
f { x: 4, y: 9 }
"#
}
#[mono_test]
fn record_optional_field_let_use_default() {
r#"
f = \r ->
{ x ? 10, y } = r
x + y
f { y: 9 }
"#
}
#[mono_test]
fn record_optional_field_function_no_use_default() {
r#"
f = \{ x ? 10, y } -> x + y
f { x: 4, y: 9 }
"#
}
#[mono_test]
fn record_optional_field_function_use_default() {
r#"
f = \{ x ? 10, y } -> x + y
f { y: 9 }
"#
}
#[mono_test]
fn quicksort_help() {
// do we still need with_larger_debug_stack?
r#"
quicksortHelp : List (Num a), I64, I64 -> List (Num a)
quicksortHelp = \list, low, high ->
if low < high then
(Pair partitionIndex partitioned) = Pair 0 []
partitioned
|> quicksortHelp low (partitionIndex - 1)
|> quicksortHelp (partitionIndex + 1) high
else
list
quicksortHelp [] 0 0
"#
}
#[mono_test]
fn quicksort_swap() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
swap = \list ->
when Pair (List.get list 0) (List.get list 0) is
Pair (Ok atI) (Ok atJ) ->
list
|> List.set 0 atJ
|> List.set 0 atI
_ ->
[]
main =
swap [ 1, 2 ]
"#
)
}
// #[ignore]
// #[mono_test]
// fn quicksort_partition_help() {
// indoc!(
// r#"
// app "test" provides [ main ] to "./platform"
// partitionHelp : I64, I64, List (Num a), I64, (Num a) -> [ Pair I64 (List (Num a)) ]
// partitionHelp = \i, j, list, high, pivot ->
// if j < high then
// when List.get list j is
// Ok value ->
// if value <= pivot then
// partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot
// else
// partitionHelp i (j + 1) list high pivot
// Err _ ->
// Pair i list
// else
// Pair i list
// main =
// partitionHelp 0 0 [] 0 0
// "#
// )
// }
// #[ignore]
// #[mono_test]
// fn quicksort_full() {
// indoc!(
// r#"
// app "test" provides [ main ] to "./platform"
// quicksortHelp : List (Num a), I64, I64 -> List (Num a)
// quicksortHelp = \list, low, high ->
// if low < high then
// (Pair partitionIndex partitioned) = partition low high list
// partitioned
// |> quicksortHelp low (partitionIndex - 1)
// |> quicksortHelp (partitionIndex + 1) high
// else
// list
// swap : I64, I64, List a -> List a
// swap = \i, j, list ->
// when Pair (List.get list i) (List.get list j) is
// Pair (Ok atI) (Ok atJ) ->
// list
// |> List.set i atJ
// |> List.set j atI
// _ ->
// []
// partition : I64, I64, List (Num a) -> [ Pair I64 (List (Num a)) ]
// partition = \low, high, initialList ->
// when List.get initialList high is
// Ok pivot ->
// when partitionHelp (low - 1) low initialList high pivot is
// Pair newI newList ->
// Pair (newI + 1) (swap (newI + 1) high newList)
// Err _ ->
// Pair (low - 1) initialList
// partitionHelp : I64, I64, List (Num a), I64, (Num a) -> [ Pair I64 (List (Num a)) ]
// partitionHelp = \i, j, list, high, pivot ->
// if j < high then
// when List.get list j is
// Ok value ->
// if value <= pivot then
// partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot
// else
// partitionHelp i (j + 1) list high pivot
// Err _ ->
// Pair i list
// else
// Pair i list
// quicksort = \originalList ->
// n = List.len originalList
// quicksortHelp originalList 0 (n - 1)
// main =
// quicksort [1,2,3]
// "#
// )
// }
#[mono_test]
fn factorial() {
r#"
factorial = \n, accum ->
when n is
0 ->
accum
_ ->
factorial (n - 1) (n * accum)
factorial 10 1
"#
}
#[mono_test]
fn is_nil() {
r#"
ConsList a : [ Cons a (ConsList a), Nil ]
isNil : ConsList a -> Bool
isNil = \list ->
when list is
Nil -> True
Cons _ _ -> False
isNil (Cons 0x2 Nil)
"#
}
#[mono_test]
#[ignore]
fn has_none() {
r#"
Maybe a : [ Just a, Nothing ]
ConsList a : [ Cons a (ConsList a), Nil ]
hasNone : ConsList (Maybe a) -> Bool
hasNone = \list ->
when list is
Nil -> False
Cons Nothing _ -> True
Cons (Just _) xs -> hasNone xs
hasNone (Cons (Just 3) Nil)
"#
}
#[mono_test]
fn mk_pair_of() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
mkPairOf = \x -> Pair x x
main =
mkPairOf [1,2,3]
"#
)
}
#[mono_test]
fn fst() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
fst = \x, _ -> x
main =
fst [1,2,3] [3,2,1]
"#
)
}
#[mono_test]
fn list_cannot_update_inplace() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
x : List I64
x = [1,2,3]
add : List I64 -> List I64
add = \y -> List.set y 0 0
main =
List.len (add x) + List.len x
"#
)
}
#[mono_test]
fn list_get() {
r#"
wrapper = \{} ->
List.get [1,2,3] 0
wrapper {}
"#
}
#[mono_test]
fn peano() {
r#"
Peano : [ S Peano, Z ]
three : Peano
three = S (S (S Z))
three
"#
}
#[mono_test]
fn peano1() {
r#"
Peano : [ S Peano, Z ]
three : Peano
three = S (S (S Z))
when three is
Z -> 0
S _ -> 1
"#
}
#[mono_test]
fn peano2() {
r#"
Peano : [ S Peano, Z ]
three : Peano
three = S (S (S Z))
when three is
S (S _) -> 1
S (_) -> 0
Z -> 0
"#
}
#[mono_test]
fn optional_when() {
r#"
f = \r ->
when r is
{ x: Blue, y ? 3 } -> y
{ x: Red, y ? 5 } -> y
a = f { x: Blue, y: 7 }
b = f { x: Blue }
c = f { x: Red, y: 11 }
d = f { x: Red }
a * b * c * d
"#
}
#[mono_test]
fn nested_pattern_match() {
r#"
Maybe a : [ Nothing, Just a ]
x : Maybe (Maybe I64)
x = Just (Just 41)
when x is
Just (Just v) -> v + 0x1
_ -> 0x1
"#
}
#[mono_test]
#[ignore]
fn linked_list_length_twice() {
r#"
LinkedList a : [ Nil, Cons a (LinkedList a) ]
nil : LinkedList I64
nil = Nil
length : LinkedList a -> I64
length = \list ->
when list is
Nil -> 0
Cons _ rest -> 1 + length rest
length nil + length nil
"#
}
#[mono_test]
fn rigids() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
swap : Nat, Nat, List a -> List a
swap = \i, j, list ->
when Pair (List.get list i) (List.get list j) is
Pair (Ok atI) (Ok atJ) ->
foo = atJ
list
|> List.set i foo
|> List.set j atI
_ ->
[]
main =
swap 0 0 [0x1]
"#
)
}
#[mono_test]
fn let_x_in_x() {
r#"
x = 5
answer =
1337
unused =
nested = 17
nested
answer
"#
}
#[mono_test]
fn let_x_in_x_indirect() {
r#"
x = 5
answer =
1337
unused =
nested = 17
i = 1
nested
{ answer, unused }.answer
"#
}
#[mono_test]
fn nested_closure() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
foo = \{} ->
x = 42
f = \{} -> x
f
main =
f = foo {}
f {}
"#
)
}
#[mono_test]
fn closure_in_list() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
foo = \{} ->
x = 41
f = \{} -> x
[ f ]
main =
items = foo {}
List.len items
"#
)
}
#[ignore]
#[mono_test]
fn somehow_drops_definitions() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
one : I64
one = 1
two : I64
two = 2
increment : I64 -> I64
increment = \x -> x + one
double : I64 -> I64
double = \x -> x * two
apply : (a -> a), a -> a
apply = \f, x -> f x
main =
apply (if True then increment else double) 42
"#
)
}
#[mono_test]
fn specialize_closures() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
apply : (a -> a), a -> a
apply = \f, x -> f x
main =
one : I64
one = 1
two : I64
two = 2
b : Bool
b = True
increment : I64 -> I64
increment = \x -> x + one
double : I64 -> I64
double = \x -> if b then x * two else x
apply (if True then increment else double) 42
"#
)
}
#[mono_test]
fn specialize_lowlevel() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
apply : (a -> a), a -> a
apply = \f, x -> f x
main =
one : I64
one = 1
two : I64
two = 2
increment : I64 -> I64
increment = \x -> x + one
double : I64 -> I64
double = \x -> x * two
(if True then increment else double) 42
"#
)
}
#[mono_test]
fn empty_list_of_function_type() {
// see https://github.com/rtfeldman/roc/issues/1732
indoc!(
r#"
app "test" provides [ main ] to "./platform"
main =
myList : List (Str -> Str)
myList = []
myClosure : Str -> Str
myClosure = \_ -> "bar"
choose =
if False then
myList
else
[ myClosure ]
when List.get choose 0 is
Ok f -> f "foo"
Err _ -> "bad!"
"#
)
}
#[mono_test]
fn monomorphized_ints() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
x = 100
f : U8, U32 -> Nat
f = \_, _ -> 18
f x x
"#
)
}
#[mono_test]
fn monomorphized_floats() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
x = 100.0
f : F32, F64 -> Nat
f = \_, _ -> 18
f x x
"#
)
}
#[mono_test]
#[ignore = "TODO"]
fn monomorphized_ints_aliased() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
y = 100
w1 = y
w2 = y
f = \_, _ -> 1
f1 : U8, U32 -> Nat
f1 = f
f2 : U32, U8 -> Nat
f2 = f
f1 w1 w2 + f2 w1 w2
"#
)
}
#[mono_test]
fn monomorphized_tag() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
b = False
f : Bool, [True, False, Idk] -> U8
f = \_, _ -> 18
f b b
"#
)
}
#[mono_test]
fn monomorphized_tag_with_aliased_args() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
b = False
c = False
a = A b c
f : [A Bool Bool] -> Nat
f = \_ -> 1
f a
"#
)
}
#[mono_test]
fn monomorphized_list() {
indoc!(
r#"
app "test" provides [main] to "./platform"
main =
l = [1, 2, 3]
f : List U8, List U16 -> Nat
f = \_, _ -> 18
f l l
"#
)
}
#[mono_test]
fn monomorphized_applied_tag() {
indoc!(
r#"
app "test" provides [ main ] to "./platform"
main =
a = A "A"
f = \x ->
when x is
A y -> y
B y -> y
f a
"#
)
}
#[mono_test]
fn aliased_polymorphic_closure() {
indoc!(
r#"
n : U8
n = 1
f = \{} -> (\a -> n)
g = f {}
g {}
"#
)
}
#[mono_test]
fn issue_2535_polymorphic_fields_referenced_in_list() {
indoc!(
r#"
app "test" provides [ nums ] to "./platform"
alpha = { a: 1, b: 2 }
nums : List U8
nums =
[
alpha.a,
alpha.b,
]
"#
)
}
// #[ignore]
// #[mono_test]
// fn static_str_closure() {
// indoc!(
// r#"
// app "test" provides [ main ] to "./platform"
// main : Str
// main =
// x = "long string that is malloced"
// f : {} -> Str
// f = (\_ -> x)
// f {}
// "#
// )
// }