Merge branch 'trunk' of github.com:rtfeldman/roc into pure-roc-list-walk

This commit is contained in:
Brian Carroll 2022-07-02 18:08:43 +01:00
commit 219e6d11cf
No known key found for this signature in database
GPG key ID: 9CF4E3BF9C4722C7
1067 changed files with 105 additions and 104 deletions

View file

@ -0,0 +1,355 @@
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
#[cfg(feature = "gen-dev")]
use crate::helpers::dev::assert_evals_to;
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_evals_to;
#[cfg(test)]
use indoc::indoc;
#[cfg(all(test, feature = "gen-llvm"))]
use roc_std::RocList;
#[cfg(all(test, feature = "gen-llvm"))]
use roc_std::RocStr;
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn hash_specialization() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
Id := U64
hash = \@Id n -> n
main = hash (@Id 1234)
"#
),
1234,
u64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn hash_specialization_multiple_add() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
Id := U64
hash = \@Id n -> n
One := {}
hash = \@One _ -> 1
main = hash (@Id 1234) + hash (@One {})
"#
),
1235,
u64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn alias_member_specialization() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
Id := U64
hash = \@Id n -> n
main =
aliasedHash = hash
aliasedHash (@Id 1234)
"#
),
1234,
u64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_constrained_in_non_member_usage() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes : a, a -> U64 | a has Hash
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \@Id n -> n
result = mulHashes (@Id 5) (@Id 7)
"#
),
35,
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_constrained_in_non_member_usage_inferred() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \@Id n -> n
result = mulHashes (@Id 5) (@Id 7)
"#
),
35,
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_constrained_in_non_member_multiple_specializations() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes : a, b -> U64 | a has Hash, b has Hash
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \@Id n -> n
Three := {}
hash = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
"#
),
300,
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_constrained_in_non_member_multiple_specializations_inferred() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \@Id n -> n
Three := {}
hash = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
"#
),
300,
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn ability_used_as_type_still_compiles() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
mulHashes : Hash, Hash -> U64
mulHashes = \x, y -> hash x * hash y
Id := U64
hash = \@Id n -> n
Three := {}
hash = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
"#
),
300,
u64
)
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn encode() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [myU8Bytes] to "./platform"
Encoder fmt := List U8, fmt -> List U8 | fmt has Format
Encoding has
toEncoder : val -> Encoder fmt | val has Encoding, fmt has Format
Format has
u8 : U8 -> Encoder fmt | fmt has Format
appendWith : List U8, Encoder fmt, fmt -> List U8 | fmt has Format
appendWith = \lst, (@Encoder doFormat), fmt -> doFormat lst fmt
toBytes : val, fmt -> List U8 | val has Encoding, fmt has Format
toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt
Linear := {}
# impl Format for Linear
u8 = \n -> @Encoder (\lst, @Linear {} -> List.append lst n)
Rgba := { r : U8, g : U8, b : U8, a : U8 }
# impl Encoding for Rgba
toEncoder = \@Rgba {r, g, b, a} ->
@Encoder \lst, fmt -> lst
|> appendWith (u8 r) fmt
|> appendWith (u8 g) fmt
|> appendWith (u8 b) fmt
|> appendWith (u8 a) fmt
myU8Bytes = toBytes (@Rgba { r: 106, g: 90, b: 205, a: 255 }) (@Linear {})
"#
),
RocList::from_slice(&[106, 90, 205, 255]),
RocList<u8>
)
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn decode() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [myU8] to "./platform"
DecodeError : [TooShort, Leftover (List U8)]
Decoder val fmt := List U8, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting
Decoding has
decoder : Decoder val fmt | val has Decoding, fmt has DecoderFormatting
DecoderFormatting has
u8 : Decoder U8 fmt | fmt has DecoderFormatting
decodeWith : List U8, Decoder val fmt, fmt -> { result: Result val DecodeError, rest: List U8 } | fmt has DecoderFormatting
decodeWith = \lst, (@Decoder doDecode), fmt -> doDecode lst fmt
fromBytes : List U8, fmt -> Result val DecodeError
| fmt has DecoderFormatting, val has Decoding
fromBytes = \lst, fmt ->
when decodeWith lst decoder fmt is
{ result, rest } ->
Result.after result \val ->
if List.isEmpty rest
then Ok val
else Err (Leftover rest)
Linear := {}
# impl DecoderFormatting for Linear
u8 = @Decoder \lst, @Linear {} ->
when List.first lst is
Ok n -> { result: Ok n, rest: List.dropFirst lst }
Err _ -> { result: Err TooShort, rest: [] }
MyU8 := U8
# impl Decoding for MyU8
decoder = @Decoder \lst, fmt ->
{ result, rest } = decodeWith lst u8 fmt
{ result: Result.map result (\n -> @MyU8 n), rest }
myU8 =
when fromBytes [15] (@Linear {}) is
Ok (@MyU8 n) -> n
_ -> 27u8
"#
),
15,
u8
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn encode_use_stdlib() {
assert_evals_to!(
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
provides [main] to "./platform"
HelloWorld := {}
toEncoder = \@HelloWorld {} ->
Encode.custom \bytes, fmt ->
bytes
|> Encode.appendWith (Encode.string "Hello, World!\n") fmt
main =
result = Str.fromUtf8 (Encode.toBytes (@HelloWorld {}) Json.format)
when result is
Ok s -> s
_ -> "<bad>"
"#
),
RocStr::from("\"Hello, World!\n\""),
RocStr
)
}

View file

@ -0,0 +1,674 @@
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
#[cfg(feature = "gen-dev")]
use crate::helpers::dev::assert_evals_to;
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_evals_to;
use indoc::indoc;
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))]
fn eq_i64() {
assert_evals_to!(
indoc!(
r#"
i : I64
i = 1
i == i
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn neq_i64() {
assert_evals_to!(
indoc!(
r#"
i : I64
i = 1
i != i
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-dev", feature = "gen-wasm"))]
fn eq_u64() {
assert_evals_to!(
indoc!(
r#"
i : U64
i = 1
i == i
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn neq_u64() {
assert_evals_to!(
indoc!(
r#"
i : U64
i = 1
i != i
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_f64() {
assert_evals_to!(
indoc!(
r#"
i : F64
i = 1
i == i
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn neq_f64() {
assert_evals_to!(
indoc!(
r#"
i : F64
i = 1
i != i
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_bool_tag() {
assert_evals_to!(
indoc!(
r#"
true : Bool
true = True
true == True
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn neq_bool_tag() {
assert_evals_to!(
indoc!(
r#"
true : Bool
true = True
true == False
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn empty_record() {
assert_evals_to!("{} == {}", true, bool);
assert_evals_to!("{} != {}", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn record() {
assert_evals_to!(
"{ x: 123, y: \"Hello\", z: 3.14 } == { x: 123, y: \"Hello\", z: 3.14 }",
true,
bool
);
assert_evals_to!(
"{ x: 234, y: \"Hello\", z: 3.14 } == { x: 123, y: \"Hello\", z: 3.14 }",
false,
bool
);
assert_evals_to!(
"{ x: 123, y: \"World\", z: 3.14 } == { x: 123, y: \"Hello\", z: 3.14 }",
false,
bool
);
assert_evals_to!(
"{ x: 123, y: \"Hello\", z: 1.11 } == { x: 123, y: \"Hello\", z: 3.14 }",
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn unit() {
assert_evals_to!("Unit == Unit", true, bool);
assert_evals_to!("Unit != Unit", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn newtype() {
assert_evals_to!("Identity 42 == Identity 42", true, bool);
assert_evals_to!("Identity 42 != Identity 42", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn small_str() {
assert_evals_to!("\"aaa\" == \"aaa\"", true, bool);
assert_evals_to!("\"aaa\" == \"bbb\"", false, bool);
assert_evals_to!("\"aaa\" != \"aaa\"", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn large_str() {
assert_evals_to!(
indoc!(
r#"
x = "Unicode can represent text values which span multiple languages"
y = "Unicode can represent text values which span multiple languages"
x == y
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
x = "Unicode can represent text values which span multiple languages"
y = "Here are some valid Roc strings"
x != y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_result_tag_true() {
assert_evals_to!(
indoc!(
r#"
x : Result I64 I64
x = Ok 1
y : Result I64 I64
y = Ok 1
x == y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_result_tag_false() {
assert_evals_to!(
indoc!(
r#"
x : Result I64 I64
x = Ok 1
y : Result I64 I64
y = Err 1
x == y
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_expr() {
assert_evals_to!(
indoc!(
r#"
Expr : [Add Expr Expr, Mul Expr Expr, Val I64, Var I64]
x : Expr
x = Val 0
y : Expr
y = Val 0
x == y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_linked_list() {
assert_evals_to!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
x : LinkedList I64
x = Nil
y : LinkedList I64
y = Nil
x == y
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
x : LinkedList I64
x = Cons 1 Nil
y : LinkedList I64
y = Cons 1 Nil
x == y
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
x : LinkedList I64
x = Cons 1 (Cons 2 Nil)
y : LinkedList I64
y = Cons 1 (Cons 2 Nil)
x == y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_linked_list_false() {
assert_evals_to!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
x : LinkedList I64
x = Cons 1 Nil
y : LinkedList I64
y = Cons 1 (Cons 2 Nil)
y == x
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn eq_linked_list_long() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
LinkedList a : [Nil, Cons a (LinkedList a)]
prependOnes = \n, tail ->
if n == 0 then
tail
else
prependOnes (n-1) (Cons 1 tail)
main =
n = 100_000
x : LinkedList I64
x = prependOnes n (Cons 999 Nil)
y : LinkedList I64
y = prependOnes n (Cons 123 Nil)
y == x
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_nullable_expr() {
assert_evals_to!(
indoc!(
r#"
Expr : [Add Expr Expr, Mul Expr Expr, Val I64, Empty]
x : Expr
x = Val 0
y : Expr
y = Add x x
x != y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn eq_rosetree() {
assert_evals_to!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
x : Rose I64
x = Rose 0 []
y : Rose I64
y = Rose 0 []
x == y
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
x : Rose I64
x = Rose 0 []
y : Rose I64
y = Rose 0 []
x != y
"#
),
false,
bool
);
assert_evals_to!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
a1 : Rose I64
a1 = Rose 999 []
a2 : Rose I64
a2 = Rose 0 [a1, a1]
a3 : Rose I64
a3 = Rose 0 [a2, a2, a2]
b1 : Rose I64
b1 = Rose 111 []
b2 : Rose I64
b2 = Rose 0 [a1, b1]
b3 : Rose I64
b3 = Rose 0 [a2, a2, b2]
a3 == b3
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn eq_different_rosetrees() {
// Requires two different equality procedures for `List (Rose I64)` and `List (Rose Str)`
// even though both appear in the mono Layout as `List(RecursivePointer)`
assert_evals_to!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
a1 : Rose I64
a1 = Rose 999 []
a2 : Rose I64
a2 = Rose 0 [a1]
b1 : Rose I64
b1 = Rose 999 []
b2 : Rose I64
b2 = Rose 0 [b1]
ab = a2 == b2
c1 : Rose Str
c1 = Rose "hello" []
c2 : Rose Str
c2 = Rose "" [c1]
d1 : Rose Str
d1 = Rose "hello" []
d2 : Rose Str
d2 = Rose "" [d1]
cd = c2 == d2
ab && cd
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[ignore]
fn rosetree_with_tag() {
// currently stack overflows in type checking
assert_evals_to!(
indoc!(
r#"
Rose a : [Rose (Result (List (Rose a)) I64)]
x : Rose I64
x = (Rose (Ok []))
y : Rose I64
y = (Rose (Ok []))
x == y
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_eq_empty() {
assert_evals_to!("[] == []", true, bool);
assert_evals_to!("[] != []", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_eq_by_length() {
assert_evals_to!("[1] == []", false, bool);
assert_evals_to!("[] == [1]", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_eq_compare_pointwise() {
assert_evals_to!("[1] == [1]", true, bool);
assert_evals_to!("[2] == [1]", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_eq_nested() {
assert_evals_to!("[[1]] == [[1]]", true, bool);
assert_evals_to!("[[2]] == [[1]]", false, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_neq_compare_pointwise() {
assert_evals_to!("[1] != [1]", false, bool);
assert_evals_to!("[2] != [1]", true, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn list_neq_nested() {
assert_evals_to!("[[1]] != [[1]]", false, bool);
assert_evals_to!("[[2]] != [[1]]", true, bool);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn compare_union_same_content() {
assert_evals_to!(
indoc!(
r#"
Foo : [A I64, B I64]
a : Foo
a = A 42
b : Foo
b = B 42
a == b
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn compare_recursive_union_same_content() {
assert_evals_to!(
indoc!(
r#"
Expr : [Add Expr Expr, Mul Expr Expr, Val1 I64, Val2 I64]
v1 : Expr
v1 = Val1 42
v2 : Expr
v2 = Val2 42
v1 == v2
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn compare_nullable_recursive_union_same_content() {
assert_evals_to!(
indoc!(
r#"
Expr : [Add Expr Expr, Mul Expr Expr, Val1 I64, Val2 I64, Empty]
v1 : Expr
v1 = Val1 42
v2 : Expr
v2 = Val2 42
v1 == v2
"#
),
false,
bool
);
}

View file

@ -0,0 +1,545 @@
#![cfg(feature = "gen-llvm")]
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
// #[cfg(feature = "gen-dev")]
// use crate::helpers::dev::assert_evals_to;
// #[cfg(feature = "gen-wasm")]
// use crate::helpers::wasm::assert_evals_to;
use indoc::indoc;
use roc_std::{RocList, RocStr};
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_empty_len() {
assert_evals_to!(
indoc!(
r#"
Dict.len Dict.empty
"#
),
0,
usize
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_insert_empty() {
assert_evals_to!(
indoc!(
r#"
Dict.insert Dict.empty 42 32
|> Dict.len
"#
),
1,
usize
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_empty_contains() {
assert_evals_to!(
indoc!(
r#"
empty : Dict I64 F64
empty = Dict.empty
Dict.contains empty 42
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_nonempty_contains() {
assert_evals_to!(
indoc!(
r#"
empty : Dict I64 F64
empty = Dict.insert Dict.empty 42 1.23
Dict.contains empty 42
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_empty_remove() {
assert_evals_to!(
indoc!(
r#"
empty : Dict I64 F64
empty = Dict.empty
empty
|> Dict.remove 42
|> Dict.len
"#
),
0,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_nonempty_remove() {
assert_evals_to!(
indoc!(
r#"
empty : Dict I64 F64
empty = Dict.insert Dict.empty 42 1.23
empty
|> Dict.remove 42
|> Dict.len
"#
),
0,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn dict_nonempty_get() {
assert_evals_to!(
indoc!(
r#"
empty : Dict I64 F64
empty = Dict.insert Dict.empty 42 1.23
withDefault = \x, def ->
when x is
Ok v -> v
Err _ -> def
empty
|> Dict.insert 42 1.23
|> Dict.get 42
|> withDefault 0
"#
),
1.23,
f64
);
assert_evals_to!(
indoc!(
r#"
withDefault = \x, def ->
when x is
Ok v -> v
Err _ -> def
Dict.empty
|> Dict.insert 42 1.23
|> Dict.get 43
|> withDefault 0
"#
),
0.0,
f64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn keys() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 I64
myDict =
Dict.empty
|> Dict.insert 0 100
|> Dict.insert 1 100
|> Dict.insert 2 100
Dict.keys myDict
"#
),
RocList::from_slice(&[0, 1, 2]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn values() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 I64
myDict =
Dict.empty
|> Dict.insert 0 100
|> Dict.insert 1 200
|> Dict.insert 2 300
Dict.values myDict
"#
),
RocList::from_slice(&[100, 200, 300]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn from_list_with_fold_simple() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 I64
myDict =
[1,2,3]
|> List.walk Dict.empty (\accum, value -> Dict.insert accum value value)
Dict.values myDict
"#
),
RocList::from_slice(&[2, 3, 1]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn from_list_with_fold_reallocates() {
assert_evals_to!(
indoc!(
r#"
range : I64, I64, List I64-> List I64
range = \low, high, accum ->
if low < high then
range (low + 1) high (List.append accum low)
else
accum
myDict : Dict I64 I64
myDict =
# 25 elements (8 + 16 + 1) is guaranteed to overflow/reallocate at least twice
range 0 25 []
|> List.walk Dict.empty (\accum, value -> Dict.insert accum value value)
Dict.values myDict
"#
),
RocList::from_slice(&[
4, 5, 20, 0, 7, 3, 1, 21, 10, 6, 13, 9, 14, 19, 2, 15, 12, 17, 16, 18, 22, 8, 11, 24,
23
]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn small_str_keys() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict Str I64
myDict =
Dict.empty
|> Dict.insert "a" 100
|> Dict.insert "b" 100
|> Dict.insert "c" 100
Dict.keys myDict
"#
),
RocList::from_slice(&[RocStr::from("c"), RocStr::from("a"), RocStr::from("b"),],),
RocList<RocStr>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn big_str_keys() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict Str I64
myDict =
Dict.empty
|> Dict.insert "Leverage agile frameworks to provide a robust" 100
|> Dict.insert "synopsis for high level overviews. Iterative approaches" 200
|> Dict.insert "to corporate strategy foster collaborative thinking to" 300
Dict.keys myDict
"#
),
RocList::from_slice(&[
RocStr::from("Leverage agile frameworks to provide a robust"),
RocStr::from("to corporate strategy foster collaborative thinking to"),
RocStr::from("synopsis for high level overviews. Iterative approaches"),
]),
RocList<RocStr>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn big_str_values() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 Str
myDict =
Dict.empty
|> Dict.insert 100 "Leverage agile frameworks to provide a robust"
|> Dict.insert 200 "synopsis for high level overviews. Iterative approaches"
|> Dict.insert 300 "to corporate strategy foster collaborative thinking to"
Dict.values myDict
"#
),
RocList::from_slice(&[
RocStr::from("Leverage agile frameworks to provide a robust"),
RocStr::from("to corporate strategy foster collaborative thinking to"),
RocStr::from("synopsis for high level overviews. Iterative approaches"),
]),
RocList<RocStr>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn unit_values() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 {}
myDict =
Dict.empty
|> Dict.insert 0 {}
|> Dict.insert 1 {}
|> Dict.insert 2 {}
|> Dict.insert 3 {}
Dict.len myDict
"#
),
4,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn single() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 {}
myDict =
Dict.single 0 {}
Dict.len myDict
"#
),
1,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn union() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 {}
myDict =
Dict.union (Dict.single 0 {}) (Dict.single 1 {})
Dict.len myDict
"#
),
2,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn union_prefer_first() {
assert_evals_to!(
indoc!(
r#"
myDict : Dict I64 I64
myDict =
Dict.union (Dict.single 0 100) (Dict.single 0 200)
Dict.values myDict
"#
),
RocList::from_slice(&[100]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn intersection() {
assert_evals_to!(
indoc!(
r#"
dict1 : Dict I64 {}
dict1 =
Dict.empty
|> Dict.insert 1 {}
|> Dict.insert 2 {}
|> Dict.insert 3 {}
|> Dict.insert 4 {}
|> Dict.insert 5 {}
dict2 : Dict I64 {}
dict2 =
Dict.empty
|> Dict.insert 0 {}
|> Dict.insert 2 {}
|> Dict.insert 4 {}
Dict.intersection dict1 dict2
|> Dict.len
"#
),
2,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn intersection_prefer_first() {
assert_evals_to!(
indoc!(
r#"
dict1 : Dict I64 I64
dict1 =
Dict.empty
|> Dict.insert 1 1
|> Dict.insert 2 2
|> Dict.insert 3 3
|> Dict.insert 4 4
|> Dict.insert 5 5
dict2 : Dict I64 I64
dict2 =
Dict.empty
|> Dict.insert 0 100
|> Dict.insert 2 200
|> Dict.insert 4 300
Dict.intersection dict1 dict2
|> Dict.values
"#
),
RocList::from_slice(&[4, 2]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn difference() {
assert_evals_to!(
indoc!(
r#"
dict1 : Dict I64 {}
dict1 =
Dict.empty
|> Dict.insert 1 {}
|> Dict.insert 2 {}
|> Dict.insert 3 {}
|> Dict.insert 4 {}
|> Dict.insert 5 {}
dict2 : Dict I64 {}
dict2 =
Dict.empty
|> Dict.insert 0 {}
|> Dict.insert 2 {}
|> Dict.insert 4 {}
Dict.difference dict1 dict2
|> Dict.len
"#
),
3,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn difference_prefer_first() {
assert_evals_to!(
indoc!(
r#"
dict1 : Dict I64 I64
dict1 =
Dict.empty
|> Dict.insert 1 1
|> Dict.insert 2 2
|> Dict.insert 3 3
|> Dict.insert 4 4
|> Dict.insert 5 5
dict2 : Dict I64 I64
dict2 =
Dict.empty
|> Dict.insert 0 100
|> Dict.insert 2 200
|> Dict.insert 4 300
Dict.difference dict1 dict2
|> Dict.values
"#
),
RocList::from_slice(&[5, 3, 1]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn walk_sum_keys() {
assert_evals_to!(
indoc!(
r#"
dict1 : Dict I64 I64
dict1 =
Dict.empty
|> Dict.insert 1 1
|> Dict.insert 2 2
|> Dict.insert 3 3
|> Dict.insert 4 4
|> Dict.insert 5 5
Dict.walk dict1 0 \k, _, a -> k + a
"#
),
15,
i64
);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,433 @@
#[cfg(feature = "gen-wasm")]
use crate::helpers::{wasm::assert_refcounts, RefCount::*};
#[allow(unused_imports)]
use indoc::indoc;
#[allow(unused_imports)]
use roc_std::{RocList, RocStr};
// A "good enough" representation of a pointer for these tests, because
// we ignore the return value. As long as it's the right stack size, it's fine.
#[allow(dead_code)]
type Pointer = usize;
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn str_inc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
[s, s, s]
"#
),
RocList<RocStr>,
&[
Live(3), // s
Live(1) // result
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn str_dealloc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
Str.isEmpty s
"#
),
bool,
&[Deallocated]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn list_int_inc() {
assert_refcounts!(
indoc!(
r#"
list = [0x111, 0x222, 0x333]
[list, list, list]
"#
),
RocList<RocList<i64>>,
&[
Live(3), // list
Live(1) // result
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn list_int_dealloc() {
assert_refcounts!(
indoc!(
r#"
list = [0x111, 0x222, 0x333]
List.len [list, list, list]
"#
),
usize,
&[
Deallocated, // list
Deallocated // result
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn list_str_inc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
list = [s, s, s]
[list, list]
"#
),
RocList<RocList<RocStr>>,
&[
Live(6), // s
Live(2), // list
Live(1) // result
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn list_str_dealloc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
list = [s, s, s]
List.len [list, list]
"#
),
usize,
&[
Deallocated, // s
Deallocated, // list
Deallocated // result
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn struct_inc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
r1 : { a: I64, b: Str, c: Str }
r1 = { a: 123, b: s, c: s }
{ y: r1, z: r1 }
"#
),
[(i64, RocStr, RocStr); 2],
&[Live(4)] // s
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn struct_dealloc() {
assert_refcounts!(
indoc!(
r#"
s = Str.concat "A long enough string " "to be heap-allocated"
r1 : { a: I64, b: Str, c: Str }
r1 = { a: 123, b: s, c: s }
r2 = { x: 456, y: r1, z: r1 }
r2.x
"#
),
i64,
&[Deallocated] // s
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_nonrecursive_inc() {
type TwoStr = (RocStr, RocStr, i64);
assert_refcounts!(
indoc!(
r#"
TwoOrNone a: [Two a a, None]
s = Str.concat "A long enough string " "to be heap-allocated"
two : TwoOrNone Str
two = Two s s
four : TwoOrNone (TwoOrNone Str)
four = Two two two
four
"#
),
(TwoStr, TwoStr, i64),
&[Live(4)]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_nonrecursive_dec() {
assert_refcounts!(
indoc!(
r#"
TwoOrNone a: [Two a a, None]
s = Str.concat "A long enough string " "to be heap-allocated"
two : TwoOrNone Str
two = Two s s
when two is
Two x _ -> x
None -> ""
"#
),
RocStr,
&[Live(1)] // s
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_recursive_inc() {
assert_refcounts!(
indoc!(
r#"
Expr : [Sym Str, Add Expr Expr]
s = Str.concat "heap_allocated" "_symbol_name"
x : Expr
x = Sym s
e : Expr
e = Add x x
Pair e e
"#
),
(Pointer, Pointer),
&[
Live(4), // s
Live(4), // sym
Live(2), // e
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_recursive_dec() {
assert_refcounts!(
indoc!(
r#"
Expr : [Sym Str, Add Expr Expr]
s = Str.concat "heap_allocated" "_symbol_name"
x : Expr
x = Sym s
e : Expr
e = Add x x
when e is
Add y _ -> y
Sym _ -> e
"#
),
Pointer,
&[
Live(1), // s
Live(1), // sym
Deallocated // e
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn refcount_different_rosetrees_inc() {
// Requires two different Inc procedures for `List (Rose I64)` and `List (Rose Str)`
// even though both appear in the mono Layout as `List(RecursivePointer)`
assert_refcounts!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
s = Str.concat "A long enough string " "to be heap-allocated"
i1 : Rose I64
i1 = Rose 999 []
s1 : Rose Str
s1 = Rose s []
i2 : Rose I64
i2 = Rose 0 [i1, i1, i1]
s2 : Rose Str
s2 = Rose "" [s1, s1]
Tuple i2 s2
"#
),
(Pointer, Pointer),
&[
Live(2), // s
Live(3), // i1
Live(2), // s1
Live(1), // [i1, i1]
Live(1), // i2
Live(1), // [s1, s1]
Live(1) // s2
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn refcount_different_rosetrees_dec() {
// Requires two different Dec procedures for `List (Rose I64)` and `List (Rose Str)`
// even though both appear in the mono Layout as `List(RecursivePointer)`
assert_refcounts!(
indoc!(
r#"
Rose a : [Rose a (List (Rose a))]
s = Str.concat "A long enough string " "to be heap-allocated"
i1 : Rose I64
i1 = Rose 999 []
s1 : Rose Str
s1 = Rose s []
i2 : Rose I64
i2 = Rose 0 [i1, i1]
s2 : Rose Str
s2 = Rose "" [s1, s1]
when (Tuple i2 s2) is
Tuple (Rose x _) _ -> x
"#
),
i64,
&[
Deallocated, // s
Deallocated, // i1
Deallocated, // s1
Deallocated, // [i1, i1]
Deallocated, // i2
Deallocated, // [s1, s1]
Deallocated, // s2
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_linked_list_inc() {
assert_refcounts!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
s = Str.concat "A long enough string " "to be heap-allocated"
linked : LinkedList Str
linked = Cons s (Cons s (Cons s Nil))
Tuple linked linked
"#
),
(Pointer, Pointer),
&[
Live(6), // s
Live(2), // Cons
Live(2), // Cons
Live(2), // Cons
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_linked_list_dec() {
assert_refcounts!(
indoc!(
r#"
LinkedList a : [Nil, Cons a (LinkedList a)]
s = Str.concat "A long enough string " "to be heap-allocated"
linked : LinkedList Str
linked = Cons s (Cons s (Cons s Nil))
when linked is
Cons x _ -> x
Nil -> ""
"#
),
RocStr,
&[
Live(1), // s
Deallocated, // Cons
Deallocated, // Cons
Deallocated, // Cons
]
);
}
#[test]
#[cfg(any(feature = "gen-wasm"))]
fn union_linked_list_long_dec() {
assert_refcounts!(
indoc!(
r#"
app "test" provides [main] to "./platform"
LinkedList a : [Nil, Cons a (LinkedList a)]
prependOnes = \n, tail ->
if n == 0 then
tail
else
prependOnes (n-1) (Cons 1 tail)
main =
n = 1_000
linked : LinkedList I64
linked = prependOnes n Nil
when linked is
Cons x _ -> x
Nil -> -1
"#
),
i64,
&[Deallocated; 1_000]
);
}

View file

@ -0,0 +1,270 @@
#![cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
// #[cfg(feature = "gen-dev")]
// use crate::helpers::dev::assert_evals_to;
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_evals_to;
use indoc::indoc;
#[allow(unused_imports)]
use roc_std::{RocResult, RocStr};
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn with_default() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Ok 2
Result.withDefault result 0
"#
),
2,
i64
);
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Err {}
Result.withDefault result 0
"#
),
0,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn result_map() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Ok 2
result
|> Result.map (\x -> x + 1)
|> Result.withDefault 0
"#
),
3,
i64
);
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Err {}
result
|> Result.map (\x -> x + 1)
|> Result.withDefault 0
"#
),
0,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn result_map_err() {
assert_evals_to!(
indoc!(
r#"
result : Result {} I64
result = Err 2
when Result.mapErr result (\x -> x + 1) is
Err n -> n
Ok _ -> 0
"#
),
3,
i64
);
assert_evals_to!(
indoc!(
r#"
result : Result {} I64
result = Ok {}
when Result.mapErr result (\x -> x + 1) is
Err n -> n
Ok _ -> 0
"#
),
0,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn err_type_var() {
assert_evals_to!(
indoc!(
r#"
Result.map (Ok 3) (\x -> x + 1)
|> Result.withDefault -1
"#
),
4,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn err_type_var_annotation() {
assert_evals_to!(
indoc!(
r#"
ok : Result I64 *
ok = Ok 3
Result.map ok (\x -> x + 1)
|> Result.withDefault -1
"#
),
4,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn err_empty_tag_union() {
assert_evals_to!(
indoc!(
r#"
ok : Result I64 []
ok = Ok 3
Result.map ok (\x -> x + 1)
|> Result.withDefault -1
"#
),
4,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn is_ok() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Ok 2
Result.isOk result
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Err {}
Result.isOk result
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn is_err() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Ok 2
Result.isErr result
"#
),
false,
bool
);
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Err {}
Result.isErr result
"#
),
true,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn roc_result_ok() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 {}
result = Ok 42
result
"#
),
RocResult::ok(42),
RocResult<i64, ()>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn roc_result_err() {
assert_evals_to!(
indoc!(
r#"
result : Result I64 Str
result = Err "foo"
result
"#
),
RocResult::err(RocStr::from("foo")),
RocResult<i64, RocStr>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn issue_2583_specialize_errors_behind_unified_branches() {
assert_evals_to!(
r#"
if True then List.first [15] else Str.toI64 ""
"#,
RocResult::ok(15i64),
RocResult<i64, bool>
)
}

View file

@ -0,0 +1,280 @@
#![cfg(feature = "gen-llvm")]
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
// #[cfg(feature = "gen-dev")]
// use crate::helpers::dev::assert_evals_to;
// #[cfg(feature = "gen-wasm")]
// use crate::helpers::wasm::assert_evals_to;
use indoc::indoc;
use roc_std::RocList;
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn empty_len() {
assert_evals_to!(
indoc!(
r#"
Set.len Set.empty
"#
),
0,
usize
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn single_len() {
assert_evals_to!(
indoc!(
r#"
Set.len (Set.single 42)
"#
),
1,
usize
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn single_to_list() {
assert_evals_to!(
indoc!(
r#"
Set.toList (Set.single 42)
"#
),
RocList::from_slice(&[42]),
RocList<i64>
);
assert_evals_to!(
indoc!(
r#"
Set.toList (Set.single 1)
"#
),
RocList::from_slice(&[1]),
RocList<i64>
);
assert_evals_to!(
indoc!(
r#"
Set.toList (Set.single 1.0)
"#
),
RocList::from_slice(&[1.0]),
RocList<f64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn insert() {
assert_evals_to!(
indoc!(
r#"
Set.empty
|> Set.insert 0
|> Set.insert 1
|> Set.insert 2
|> Set.toList
"#
),
RocList::from_slice(&[0, 1, 2]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn remove() {
assert_evals_to!(
indoc!(
r#"
Set.empty
|> Set.insert 0
|> Set.insert 1
|> Set.remove 1
|> Set.remove 2
|> Set.toList
"#
),
RocList::from_slice(&[0]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn union() {
assert_evals_to!(
indoc!(
r#"
set1 : Set I64
set1 = Set.fromList [1,2]
set2 : Set I64
set2 = Set.fromList [1,3,4]
Set.union set1 set2
|> Set.toList
"#
),
RocList::from_slice(&[4, 2, 3, 1]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn difference() {
assert_evals_to!(
indoc!(
r#"
set1 : Set I64
set1 = Set.fromList [1,2]
set2 : Set I64
set2 = Set.fromList [1,3,4]
Set.difference set1 set2
|> Set.toList
"#
),
RocList::from_slice(&[2]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn intersection() {
assert_evals_to!(
indoc!(
r#"
set1 : Set I64
set1 = Set.fromList [1,2]
set2 : Set I64
set2 = Set.fromList [1,3,4]
Set.intersection set1 set2
|> Set.toList
"#
),
RocList::from_slice(&[1]),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn walk_sum() {
assert_evals_to!(
indoc!(
r#"
Set.walk (Set.fromList [1,2,3]) 0 (\x, y -> x + y)
"#
),
6,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn contains() {
assert_evals_to!(
indoc!(
r#"
Set.contains (Set.fromList [1,3,4]) 4
"#
),
true,
bool
);
assert_evals_to!(
indoc!(
r#"
Set.contains (Set.fromList [1,3,4]) 2
"#
),
false,
bool
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn from_list() {
assert_evals_to!(
indoc!(
r#"
[1,2,2,3,1,4]
|> Set.fromList
|> Set.toList
"#
),
RocList::from_slice(&[4, 2, 3, 1]),
RocList<i64>
);
assert_evals_to!(
indoc!(
r#"
empty : List I64
empty = []
empty
|> Set.fromList
|> Set.toList
"#
),
RocList::<i64>::default(),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn from_list_void() {
assert_evals_to!(
indoc!(
r#"
[]
|> Set.fromList
|> Set.toList
"#
),
RocList::<i64>::default(),
RocList<i64>
);
}
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn from_list_result() {
assert_evals_to!(
indoc!(
r#"
x : Result Str {}
x = Ok "foo"
[x]
|> Set.fromList
|> Set.toList
|> List.len
"#
),
1,
i64
);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,372 @@
<html>
<head>
<style>
body {
background-color: #111;
color: #ccc;
font-family: sans-serif;
font-size: 18px;
}
section {
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
}
h1 {
margin: 32px auto;
}
li {
margin: 8px;
}
code {
color: #aaa;
background-color: #000;
padding: 1px 4px;
font-size: 16px;
border-radius: 6px;
}
input,
button {
font-size: 20px;
font-weight: bold;
padding: 4px 12px;
}
small {
font-style: italic;
}
#error {
color: #f00;
}
.controls {
margin-top: 64px;
display: flex;
flex-direction: column;
}
.controls button {
margin: 16px;
}
.row {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 24px;
}
.row-file label {
margin: 0 32px;
}
</style>
</head>
<body>
<section>
<h1>Debug Wasm tests in the browser!</h1>
<p>
You can step through the generated code instruction-by-instruction, and
examine memory contents
</p>
<h3>Steps</h3>
<ul>
<li>
In <code>gen_wasm/src/lib.rs</code>, set
<code>DEBUG_LOG_SETTINGS.keep_test_binary = true</code>
</li>
<li>Run <code>cargo test-gen-wasm -- my_test --nocapture</code></li>
<li>
Look for the path written to the console for
<code>final.wasm</code> and select it in the file picker below
</li>
<li>
Open the browser DevTools <br />
<small> Control+Shift+I or Command+Option+I or F12 </small>
</li>
<li>
Click one of the buttons below, depending on what kind of test it is.
<br />
<small>
Only one of them will work. The other will probably crash or
something.
</small>
</li>
<li>
The debugger should pause just before entering the first Wasm call.
Step into a couple of Wasm calls until you reach your test code in
<code>$#UserApp_main_1</code>
</li>
<li>
Chrome DevTools now has a Memory Inspector panel! In the debugger,
find <code>Module -> memories -> $memory</code>. Right click and
select "Reveal in Memory Inspector"
</li>
</ul>
<div class="controls">
<div class="row row-file">
<label for="wasm-file">Select final.wasm</label>
<input id="wasm-file" type="file" />
</div>
<div id="error" class="row"></div>
<div class="row">
<button id="button-expression">Run as Roc expression test</button>
<button id="button-refcount">Run as reference counting test</button>
</div>
</div>
</section>
<script>
const file_input = document.getElementById("wasm-file");
const button_expression = document.getElementById("button-expression");
const button_refcount = document.getElementById("button-refcount");
const error_box = document.getElementById("error");
button_expression.onclick = runExpressionTest;
button_refcount.onclick = runRefcountTest;
file_input.onchange = function () {
error_box.innerHTML = "";
};
async function runExpressionTest() {
const file = getFile();
const instance = await compileFileToInstance(file);
debugger; // Next call is Wasm! Step into test_wrapper, then $#UserApp_main_1
instance.exports.test_wrapper();
}
async function runRefcountTest() {
const file = getFile();
const instance = await compileFileToInstance(file);
const MAX_ALLOCATIONS = 100;
const refcount_vector_addr =
instance.exports.init_refcount_test(MAX_ALLOCATIONS);
debugger; // Next call is Wasm! Step into test_wrapper, then $#UserApp_main_1
instance.exports.test_wrapper();
const words = new Uint32Array(instance.exports.memory.buffer);
function deref(addr8) {
return words[addr8 >> 2];
}
const actual_len = deref(refcount_vector_addr);
const rc_pointers = [];
for (let i = 0; i < actual_len; i++) {
const offset = (1 + i) << 2;
const rc_ptr = deref(refcount_vector_addr + offset);
rc_pointers.push(rc_ptr);
}
const rc_encoded = rc_pointers.map((ptr) => ptr && deref(ptr));
const rc_encoded_hex = rc_encoded.map((x) =>
x ? x.toString(16) : "(deallocated)"
);
const rc_values = rc_encoded.map((x) => x && x - 0x80000000 + 1);
console.log({ rc_values, rc_encoded_hex });
}
function getFile() {
const { files } = file_input;
if (!files.length) {
const msg = "Select a file!";
error_box.innerHTML = msg;
throw new Error(msg);
}
return files[0];
}
async function compileFileToInstance(file) {
const buffer = await file.arrayBuffer();
const wasiLinkObject = {};
const importObject = createFakeWasiImports(wasiLinkObject);
const result = await WebAssembly.instantiate(buffer, importObject);
wasiLinkObject.memory8 = new Uint8Array(
result.instance.exports.memory.buffer
);
wasiLinkObject.memory32 = new Uint32Array(
result.instance.exports.memory.buffer
);
return result.instance;
}
// If you print to stdout (for example in the platform), it calls these WASI imports.
// This implementation uses console.log
function createFakeWasiImports(wasiLinkObject) {
const decoder = new TextDecoder();
// fd_close : (i32) -> i32
// Close a file descriptor. Note: This is similar to close in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_close.html
function fd_close(fd) {
console.warn(`fd_close: ${{ fd }}`);
return 0; // error code
}
// fd_fdstat_get : (i32, i32) -> i32
// Get the attributes of a file descriptor.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_fdstat_get.html
function fd_fdstat_get(fd, stat_mut_ptr) {
/*
Tell WASI that stdout is a tty (no seek or tell)
https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/isatty.c
*Not* a tty if:
(statbuf.fs_filetype != __WASI_FILETYPE_CHARACTER_DEVICE ||
(statbuf.fs_rights_base & (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL)) != 0)
So it's sufficient to set:
.fs_filetype = __WASI_FILETYPE_CHARACTER_DEVICE
.fs_rights_base = 0
https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/headers/public/wasi/api.h
typedef uint8_t __wasi_filetype_t;
typedef uint16_t __wasi_fdflags_t;
typedef uint64_t __wasi_rights_t;
#define __WASI_FILETYPE_CHARACTER_DEVICE (UINT8_C(2))
typedef struct __wasi_fdstat_t { // 24 bytes total
__wasi_filetype_t fs_filetype; // 1 byte
// 1 byte padding
__wasi_fdflags_t fs_flags; // 2 bytes
// 4 bytes padding
__wasi_rights_t fs_rights_base; // 8 bytes
__wasi_rights_t fs_rights_inheriting; // 8 bytes
} __wasi_fdstat_t;
*/
// console.warn(`fd_fdstat_get: ${{ fd, stat_mut_ptr }}`);
const WASI_FILETYPE_CHARACTER_DEVICE = 2;
wasiLinkObject.memory8[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE;
wasiLinkObject.memory8
.slice(stat_mut_ptr + 1, stat_mut_ptr + 24)
.fill(0);
return 0; // error code
}
// fd_seek : (i32, i64, i32, i32) -> i32
// Move the offset of a file descriptor. Note: This is similar to lseek in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_seek.html
function fd_seek(fd, offset, whence, newoffset_mut_ptr) {
console.warn(`fd_seek: ${{ fd, offset, whence, newoffset_mut_ptr }}`);
return 0;
}
// fd_write : (i32, i32, i32, i32) -> i32
// Write to a file descriptor. Note: This is similar to `writev` in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_write.html
function fd_write(fd, iovs_ptr, iovs_len, nwritten_mut_ptr) {
let string_buffer = "";
let nwritten = 0;
const STDOUT = 1;
for (let i = 0; i < iovs_len; i++) {
const index32 = iovs_ptr >> 2;
const base = wasiLinkObject.memory32[index32];
const len = wasiLinkObject.memory32[index32 + 1];
iovs_ptr += 8;
if (!len) continue;
nwritten += len;
// For some reason we often get negative-looking buffer lengths with junk data.
// Just skip the console.log, but still increase nwritten or it will loop forever.
// Dunno why this happens, but it's working fine for printf debugging ¯\_(ツ)_/¯
if (len >> 31) {
break;
}
const buf = wasiLinkObject.memory8.slice(base, base + len);
const chunk = decoder.decode(buf);
string_buffer += chunk;
}
wasiLinkObject.memory32[nwritten_mut_ptr >> 2] = nwritten;
if (string_buffer) {
if (fd === STDOUT) {
console.log(string_buffer);
} else {
console.error(string_buffer);
}
}
return 0;
}
// proc_exit : (i32) -> nil
function proc_exit(exit_code) {
throw new Error(`Wasm exited with code ${exit_code}`);
}
// Signatures from wasm_test_platform.o
const sig2 = (i32) => {};
const sig6 = (i32a, i32b) => 0;
const sig7 = (i32a, i32b, i32c) => 0;
const sig9 = (i32a, i64b, i32c) => 0;
const sig10 = (i32a, i64b, i64c, i32d) => 0;
const sig11 = (i32a, i64b, i64c) => 0;
const sig12 = (i32a) => 0;
const sig13 = (i32a, i64b) => 0;
const sig14 = (i32a, i32b, i32c, i64d, i32e) => 0;
const sig15 = (i32a, i32b, i32c, i32d) => 0;
const sig16 = (i32a, i64b, i32c, i32d) => 0;
const sig17 = (i32a, i32b, i32c, i32d, i32e) => 0;
const sig18 = (i32a, i32b, i32c, i32d, i64e, i64f, i32g) => 0;
const sig19 = (i32a, i32b, i32c, i32d, i32e, i32f, i32g) => 0;
const sig20 = (i32a, i32b, i32c, i32d, i32e, i64f, i64g, i32h, i32i) =>
0;
const sig21 = (i32a, i32b, i32c, i32d, i32e, i32f) => 0;
const sig22 = () => 0;
return {
wasi_snapshot_preview1: {
args_get: sig6,
args_sizes_get: sig6,
environ_get: sig6,
environ_sizes_get: sig6,
clock_res_get: sig6,
clock_time_get: sig9,
fd_advise: sig10,
fd_allocate: sig11,
fd_close,
fd_datasync: sig12,
fd_fdstat_get,
fd_fdstat_set_flags: sig6,
fd_fdstat_set_rights: sig11,
fd_filestat_get: sig6,
fd_filestat_set_size: sig13,
fd_filestat_set_times: sig10,
fd_pread: sig14,
fd_prestat_get: sig6,
fd_prestat_dir_name: sig7,
fd_pwrite: sig14,
fd_read: sig15,
fd_readdir: sig14,
fd_renumber: sig6,
fd_seek,
fd_sync: sig12,
fd_tell: sig6,
fd_write,
path_create_directory: sig7,
path_filestat_get: sig17,
path_filestat_set_times: sig18,
path_link: sig19,
path_open: sig20,
path_readlink: sig21,
path_remove_directory: sig7,
path_rename: sig21,
path_symlink: sig17,
path_unlink_file: sig7,
poll_oneoff: sig15,
proc_exit,
proc_raise: sig12,
sched_yield: sig22,
random_get: sig6,
sock_recv: sig21,
sock_send: sig17,
sock_shutdown: sig6,
},
};
}
</script>
</body>
</html>

View file

@ -0,0 +1,265 @@
use libloading::Library;
use roc_build::link::{link, LinkType};
use roc_builtins::bitcode;
use roc_collections::all::MutMap;
use roc_load::Threading;
use roc_region::all::LineInfo;
use tempfile::tempdir;
#[allow(unused_imports)]
use roc_mono::ir::pretty_print_ir_symbols;
#[allow(dead_code)]
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
}
#[allow(dead_code)]
pub fn helper(
arena: &bumpalo::Bump,
src: &str,
_leak: bool,
lazy_literals: bool,
) -> (String, Vec<roc_problem::can::Problem>, Library) {
use std::path::{Path, PathBuf};
let dir = tempdir().unwrap();
let filename = PathBuf::from("Test.roc");
let src_dir = Path::new("fake/test/path");
let app_o_file = dir.path().join("app.o");
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 loaded = roc_load::load_and_monomorphize_from_str(
arena,
filename,
module_src,
src_dir,
Default::default(),
roc_target::TargetInfo::default_x86_64(),
roc_reporting::report::RenderTarget::ColorTerminal,
Threading::Single,
);
let mut loaded = loaded.expect("failed to load module");
use roc_load::MonomorphizedModule;
let MonomorphizedModule {
module_id,
procedures,
mut interns,
exposed_to_host,
..
} = loaded;
// You can comment and uncomment this block out to get more useful information
// while you're working on the dev backend!
{
// println!("=========== Procedures ==========");
// if pretty_print_ir_symbols() {
// println!("");
// for proc in procedures.values() {
// println!("{}", proc.to_pretty(200));
// }
// } else {
// println!("{:?}", procedures.values());
// }
// println!("=================================\n");
// println!("=========== Interns ==========");
// println!("{:?}", interns);
// println!("=================================\n");
// println!("=========== Exposed ==========");
// println!("{:?}", exposed_to_host);
// println!("=================================\n");
}
debug_assert_eq!(exposed_to_host.values.len(), 1);
let main_fn_symbol = loaded.entry_point.symbol;
let main_fn_layout = loaded.entry_point.layout;
let mut layout_ids = roc_mono::layout::LayoutIds::default();
let main_fn_name = layout_ids
.get_toplevel(main_fn_symbol, &main_fn_layout)
.to_exposed_symbol_string(main_fn_symbol, &interns);
let mut lines = Vec::new();
// errors whose reporting we delay (so we can see that code gen generates runtime errors)
let mut delayed_errors = Vec::new();
for (home, (module_path, src)) in loaded.sources {
use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE};
let can_problems = loaded.can_problems.remove(&home).unwrap_or_default();
let type_problems = loaded.type_problems.remove(&home).unwrap_or_default();
let error_count = can_problems.len() + type_problems.len();
if error_count == 0 {
continue;
}
let line_info = LineInfo::new(&src);
let src_lines: Vec<&str> = src.split('\n').collect();
let palette = DEFAULT_PALETTE;
// Report parsing and canonicalization problems
let alloc = RocDocAllocator::new(&src_lines, home, &interns);
use roc_problem::can::Problem::*;
for problem in can_problems.into_iter() {
// Ignore "unused" problems
match problem {
UnusedDef(_, _) | UnusedArgument(_, _, _) | UnusedImport(_, _) => {
delayed_errors.push(problem);
continue;
}
_ => {
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
lines.push(buf);
}
}
}
for problem in type_problems {
if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) {
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
lines.push(buf);
}
}
}
if !lines.is_empty() {
println!("{}", lines.join("\n"));
assert_eq!(0, 1, "Mistakes were made");
}
let env = roc_gen_dev::Env {
arena,
module_id,
exposed_to_host: exposed_to_host.values.keys().copied().collect(),
lazy_literals,
generate_allocators: true, // Needed for testing, since we don't have a platform
};
let target = target_lexicon::Triple::host();
let module_object = roc_gen_dev::build_module(&env, &mut interns, &target, procedures);
let module_out = module_object
.write()
.expect("failed to build output object");
std::fs::write(&app_o_file, module_out).expect("failed to write object to file");
// std::fs::copy(&app_o_file, "/tmp/app.o").unwrap();
let (mut child, dylib_path) = link(
&target,
app_o_file.clone(),
// Long term we probably want a smarter way to link in zig builtins.
// With the current method all methods are kept and it adds about 100k to all outputs.
&[
app_o_file.to_str().unwrap(),
bitcode::BUILTINS_HOST_OBJ_PATH,
],
LinkType::Dylib,
)
.expect("failed to link dynamic library");
child.wait().unwrap();
// Load the dylib
let path = dylib_path.as_path().to_str().unwrap();
// std::fs::copy(&path, "/tmp/libapp.so").unwrap();
let lib = unsafe { Library::new(path) }.expect("failed to load shared library");
(main_fn_name, delayed_errors, lib)
}
#[allow(unused_macros)]
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {{
assert_evals_to!($src, $expected, $ty, (|val| val));
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
// Same as above, except with an additional transformation argument.
{
assert_evals_to!($src, $expected, $ty, $transform, true);
}
};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr) => {
// Run both with and without lazy literal optimization.
{
assert_evals_to!($src, $expected, $ty, $transform, $leak, false);
}
{
assert_evals_to!($src, $expected, $ty, $transform, $leak, true);
}
};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr, $lazy_literals:expr) => {
use bumpalo::Bump;
use roc_gen_dev::run_jit_function_raw;
let arena = Bump::new();
let (main_fn_name, errors, lib) =
$crate::helpers::dev::helper(&arena, $src, $leak, $lazy_literals);
let transform = |success| {
let expected = $expected;
let given = $transform(success);
assert_eq!(&given, &expected);
};
run_jit_function_raw!(lib, main_fn_name, $ty, transform, errors)
};
}
#[allow(unused_macros)]
macro_rules! assert_expect_failed {
($src:expr, $expected:expr, $ty:ty, $failures:expr) => {{
use bumpalo::Bump;
use roc_gen_dev::run_jit_function_raw;
let stdlib = roc_builtins::std::standard_stdlib();
let arena = Bump::new();
let (main_fn_name, errors, lib) =
$crate::helpers::dev::helper(&arena, $src, stdlib, true, true);
let transform = |success| {
let expected = $expected;
assert_eq!(&success, &expected);
};
run_jit_function_raw!(lib, main_fn_name, $ty, transform, errors);
}};
}
#[allow(unused_imports)]
pub(crate) use assert_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_expect_failed;

View file

@ -0,0 +1,169 @@
use roc_gen_wasm::wasm32_sized::Wasm32Sized;
use roc_std::{RocDec, RocList, RocOrder, RocStr};
use std::convert::TryInto;
pub trait FromWasmerMemory: Wasm32Sized {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self;
}
macro_rules! from_wasm_memory_primitive_decode {
($type_name:ident) => {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
use core::mem::MaybeUninit;
let mut output: MaybeUninit<Self> = MaybeUninit::uninit();
let width = std::mem::size_of::<Self>();
let ptr = output.as_mut_ptr();
let raw_ptr = ptr as *mut u8;
let slice = unsafe { std::slice::from_raw_parts_mut(raw_ptr, width) };
let memory_bytes: &[u8] = unsafe { memory.data_unchecked() };
let index = offset as usize;
let wasm_slice = &memory_bytes[index..][..width];
slice.copy_from_slice(wasm_slice);
unsafe { output.assume_init() }
}
};
}
macro_rules! from_wasm_memory_primitive {
($($type_name:ident ,)+) => {
$(
impl FromWasmerMemory for $type_name {
from_wasm_memory_primitive_decode!($type_name);
}
)*
}
}
from_wasm_memory_primitive!(
u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, bool, RocDec, RocOrder,
);
impl FromWasmerMemory for () {
fn decode(_: &wasmer::Memory, _: u32) -> Self {}
}
impl FromWasmerMemory for RocStr {
fn decode(memory: &wasmer::Memory, addr: u32) -> Self {
let memory_bytes = unsafe { memory.data_unchecked() };
let index = addr as usize;
let mut str_bytes = [0; 12];
str_bytes.copy_from_slice(&memory_bytes[index..][..12]);
let str_words: &[u32; 3] = unsafe { std::mem::transmute(&str_bytes) };
let big_elem_ptr = str_words[0] as usize;
let big_length = str_words[1] as usize;
let last_byte = str_bytes[11];
let is_small_str = last_byte >= 0x80;
let slice = if is_small_str {
let small_length = (last_byte & 0x7f) as usize;
&str_bytes[0..small_length]
} else {
&memory_bytes[big_elem_ptr..][..big_length]
};
unsafe { RocStr::from_slice_unchecked(slice) }
}
}
impl<T: FromWasmerMemory + Clone> FromWasmerMemory for RocList<T> {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
let bytes = <u64 as FromWasmerMemory>::decode(memory, offset);
let length = (bytes >> 32) as u32;
let elements = bytes as u32;
let mut items = Vec::with_capacity(length as usize);
for i in 0..length {
let item = <T as FromWasmerMemory>::decode(
memory,
elements + i * <T as Wasm32Sized>::SIZE_OF_WASM as u32,
);
items.push(item);
}
RocList::from_slice(&items)
}
}
impl<T: FromWasmerMemory> FromWasmerMemory for &'_ T {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
let elements = <u32 as FromWasmerMemory>::decode(memory, offset);
let actual = <T as FromWasmerMemory>::decode(memory, elements);
let b = Box::new(actual);
std::boxed::Box::<T>::leak(b)
}
}
impl<T: FromWasmerMemory + Clone, const N: usize> FromWasmerMemory for [T; N] {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
let memory_bytes: &[u8] = unsafe { memory.data_unchecked() };
let index = offset as usize;
debug_assert!(memory_bytes.len() >= index + (N * <T as Wasm32Sized>::SIZE_OF_WASM));
let slice_bytes: &[u8] = &memory_bytes[index..][..N];
let slice: &[T] = unsafe { std::mem::transmute(slice_bytes) };
let array: &[T; N] = slice.try_into().expect("incorrect length");
array.clone()
}
}
impl FromWasmerMemory for usize {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
<u32 as FromWasmerMemory>::decode(memory, offset) as usize
}
}
impl<T: FromWasmerMemory, U: FromWasmerMemory> FromWasmerMemory for (T, U) {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
debug_assert!(
T::ALIGN_OF_WASM >= U::ALIGN_OF_WASM,
"this function does not handle alignment"
);
let t = <T as FromWasmerMemory>::decode(memory, offset);
let u = <U as FromWasmerMemory>::decode(memory, offset + T::ACTUAL_WIDTH as u32);
(t, u)
}
}
impl<T: FromWasmerMemory, U: FromWasmerMemory, V: FromWasmerMemory> FromWasmerMemory for (T, U, V) {
fn decode(memory: &wasmer::Memory, offset: u32) -> Self {
debug_assert!(
T::ALIGN_OF_WASM >= U::ALIGN_OF_WASM,
"this function does not handle alignment"
);
debug_assert!(
U::ALIGN_OF_WASM >= V::ALIGN_OF_WASM,
"this function does not handle alignment"
);
let t = <T as FromWasmerMemory>::decode(memory, offset);
let u = <U as FromWasmerMemory>::decode(memory, offset + T::ACTUAL_WIDTH as u32);
let v = <V as FromWasmerMemory>::decode(
memory,
offset + T::ACTUAL_WIDTH as u32 + U::ACTUAL_WIDTH as u32,
);
(t, u, v)
}
}

View file

@ -0,0 +1,673 @@
use crate::helpers::from_wasmer_memory::FromWasmerMemory;
use inkwell::module::Module;
use libloading::Library;
use roc_build::link::module_to_dylib;
use roc_build::program::FunctionIterator;
use roc_collections::all::MutSet;
use roc_gen_llvm::llvm::externs::add_default_roc_externs;
use roc_load::Threading;
use roc_mono::ir::OptLevel;
use roc_region::all::LineInfo;
use roc_reporting::report::RenderTarget;
use target_lexicon::Triple;
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
}
#[allow(clippy::too_many_arguments)]
fn create_llvm_module<'a>(
arena: &'a bumpalo::Bump,
src: &str,
is_gen_test: bool,
ignore_problems: bool,
context: &'a inkwell::context::Context,
target: &Triple,
opt_level: OptLevel,
) -> (&'static str, String, &'a Module<'a>) {
use std::path::{Path, PathBuf};
let target_info = roc_target::TargetInfo::from(target);
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 loaded = roc_load::load_and_monomorphize_from_str(
arena,
filename,
module_src,
src_dir,
Default::default(),
target_info,
RenderTarget::ColorTerminal,
Threading::Single,
);
let mut loaded = match loaded {
Ok(x) => x,
Err(roc_load::LoadingProblem::FormattedReport(report)) => {
println!("{}", report);
panic!();
}
Err(e) => panic!("{:?}", e),
};
use roc_load::MonomorphizedModule;
let MonomorphizedModule {
procedures,
entry_point,
interns,
..
} = loaded;
let mut lines = Vec::new();
// errors whose reporting we delay (so we can see that code gen generates runtime errors)
let mut delayed_errors = Vec::new();
for (home, (module_path, src)) in loaded.sources {
use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE};
let can_problems = loaded.can_problems.remove(&home).unwrap_or_default();
let type_problems = loaded.type_problems.remove(&home).unwrap_or_default();
let error_count = can_problems.len() + type_problems.len();
if error_count == 0 {
continue;
}
let line_info = LineInfo::new(&src);
let src_lines: Vec<&str> = src.split('\n').collect();
let palette = DEFAULT_PALETTE;
// Report parsing and canonicalization problems
let alloc = RocDocAllocator::new(&src_lines, home, &interns);
use roc_problem::can::Problem::*;
for problem in can_problems.into_iter() {
match problem {
// Ignore "unused" problems
UnusedDef(_, _)
| UnusedArgument(_, _, _)
| UnusedImport(_, _)
| RuntimeError(_)
| UnsupportedPattern(_, _)
| ExposedButNotDefined(_) => {
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
delayed_errors.push(buf.clone());
lines.push(buf);
}
// We should be able to compile even when abilities are used as types
AbilityUsedAsType(..) => {}
_ => {
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
lines.push(buf);
}
}
}
for problem in type_problems {
if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) {
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
lines.push(buf);
}
}
}
if !lines.is_empty() {
println!("{}", lines.join("\n"));
// only crash at this point if there were no delayed_errors
if delayed_errors.is_empty() && !ignore_problems {
assert_eq!(0, 1, "Mistakes were made");
}
}
let builder = context.create_builder();
let module = roc_gen_llvm::llvm::build::module_from_builtins(target, context, "app");
let module = arena.alloc(module);
let (module_pass, function_pass) =
roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level);
let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module);
// mark our zig-defined builtins as internal
use inkwell::attributes::{Attribute, AttributeLoc};
use inkwell::module::Linkage;
let kind_id = Attribute::get_named_enum_kind_id("alwaysinline");
debug_assert!(kind_id > 0);
let attr = context.create_enum_attribute(kind_id, 1);
for function in FunctionIterator::from_module(module) {
let name = function.get_name().to_str().unwrap();
if name.starts_with("roc_builtins") {
if name.starts_with("roc_builtins.expect") {
function.set_linkage(Linkage::External);
} else {
function.set_linkage(Linkage::Internal);
}
}
if name.starts_with("roc_builtins.dict") {
function.add_attribute(AttributeLoc::Function, attr);
}
if name.starts_with("roc_builtins.list") {
function.add_attribute(AttributeLoc::Function, attr);
}
}
// Compile and add all the Procs before adding main
let env = roc_gen_llvm::llvm::build::Env {
arena,
builder: &builder,
dibuilder: &dibuilder,
compile_unit: &compile_unit,
context,
interns,
module,
target_info,
is_gen_test,
// important! we don't want any procedures to get the C calling convention
exposed_to_host: MutSet::default(),
};
// strip Zig debug stuff
module.strip_debug_info();
// Add roc_alloc, roc_realloc, and roc_dealloc, since the repl has no
// platform to provide them.
add_default_roc_externs(&env);
let (main_fn_name, main_fn) = roc_gen_llvm::llvm::build::build_procedures_return_main(
&env,
opt_level,
procedures,
entry_point,
);
env.dibuilder.finalize();
// strip all debug info: we don't use it at the moment and causes weird validation issues
module.strip_debug_info();
// Uncomment this to see the module's un-optimized LLVM instruction output:
// env.module.print_to_stderr();
if main_fn.verify(true) {
function_pass.run_on(&main_fn);
} else {
panic!("Main function {} failed LLVM verification in NON-OPTIMIZED build. Uncomment things nearby to see more details.", main_fn_name);
}
module_pass.run_on(env.module);
// Verify the module
if let Err(errors) = env.module.verify() {
panic!("Errors defining module:\n\n{}", errors.to_string());
}
// Uncomment this to see the module's optimized LLVM instruction output:
// env.module.print_to_stderr();
(main_fn_name, delayed_errors.join("\n"), env.module)
}
#[allow(dead_code)]
#[inline(never)]
pub fn helper<'a>(
arena: &'a bumpalo::Bump,
src: &str,
is_gen_test: bool,
ignore_problems: bool,
context: &'a inkwell::context::Context,
) -> (&'static str, String, Library) {
let target = target_lexicon::Triple::host();
let opt_level = if cfg!(debug_assertions) {
OptLevel::Normal
} else {
OptLevel::Optimize
};
let (main_fn_name, delayed_errors, module) = create_llvm_module(
arena,
src,
is_gen_test,
ignore_problems,
context,
&target,
opt_level,
);
let lib =
module_to_dylib(module, &target, opt_level).expect("Error loading compiled dylib for test");
(main_fn_name, delayed_errors, lib)
}
fn wasm32_target_tripple() -> Triple {
use target_lexicon::{Architecture, BinaryFormat};
let mut triple = Triple::unknown();
triple.architecture = Architecture::Wasm32;
triple.binary_format = BinaryFormat::Wasm;
triple
}
#[allow(dead_code)]
pub fn helper_wasm<'a>(
arena: &'a bumpalo::Bump,
src: &str,
_is_gen_test: bool,
ignore_problems: bool,
context: &'a inkwell::context::Context,
) -> wasmer::Instance {
let target = wasm32_target_tripple();
let opt_level = if cfg!(debug_assertions) {
OptLevel::Normal
} else {
OptLevel::Optimize
};
let is_gen_test = false;
let (_main_fn_name, _delayed_errors, llvm_module) = create_llvm_module(
arena,
src,
is_gen_test,
ignore_problems,
context,
&target,
opt_level,
);
use inkwell::targets::{InitializationConfig, Target, TargetTriple};
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
// let zig_global_cache_path = std::path::PathBuf::from("/home/folkertdev/roc/wasm/mess");
let test_a_path = dir_path.join("test.a");
let test_wasm_path = dir_path.join("libmain.wasm");
Target::initialize_webassembly(&InitializationConfig::default());
let triple = TargetTriple::create("wasm32-unknown-unknown-wasm");
llvm_module.set_triple(&triple);
llvm_module.set_source_file_name("Test.roc");
let target_machine = Target::from_name("wasm32")
.unwrap()
.create_target_machine(
&triple,
"",
"", // TODO: this probably should be TargetMachine::get_host_cpu_features() to enable all features.
inkwell::OptimizationLevel::None,
inkwell::targets::RelocMode::Default,
inkwell::targets::CodeModel::Default,
)
.unwrap();
let file_type = inkwell::targets::FileType::Object;
target_machine
.write_to_file(llvm_module, file_type, &test_a_path)
.unwrap();
use std::process::Command;
Command::new(&crate::helpers::zig_executable())
.current_dir(dir_path)
.args(&[
"wasm-ld",
"/home/folkertdev/roc/wasm/libmain.a",
"/home/folkertdev/roc/wasm/libc.a",
test_a_path.to_str().unwrap(),
"-o",
test_wasm_path.to_str().unwrap(),
"--export-dynamic",
"--allow-undefined",
"--no-entry",
])
.status()
.unwrap();
// now, do wasmer stuff
use wasmer::{Function, Instance, Module, Store};
let store = Store::default();
let module = Module::from_file(&store, &test_wasm_path).unwrap();
// First, we create the `WasiEnv`
use wasmer_wasi::WasiState;
let mut wasi_env = WasiState::new("hello")
// .args(&["world"])
// .env("KEY", "Value")
.finalize()
.unwrap();
// Then, we get the import object related to our WASI
// and attach it to the Wasm instance.
let mut import_object = wasi_env
.import_object(&module)
.unwrap_or_else(|_| wasmer::imports!());
{
let mut exts = wasmer::Exports::new();
let main_function = Function::new_native(&store, fake_wasm_main_function);
let ext = wasmer::Extern::Function(main_function);
exts.insert("main", ext);
let main_function = Function::new_native(&store, wasm_roc_panic);
let ext = wasmer::Extern::Function(main_function);
exts.insert("roc_panic", ext);
import_object.register("env", exts);
}
Instance::new(&module, &import_object).unwrap()
}
#[allow(dead_code)]
fn wasm_roc_panic(address: u32, tag_id: u32) {
match tag_id {
0 => {
let mut string = "";
MEMORY.with(|f| {
let memory = f.borrow().unwrap();
let memory_bytes: &[u8] = unsafe { memory.data_unchecked() };
let index = address as usize;
let slice = &memory_bytes[index..];
let c_ptr: *const u8 = slice.as_ptr();
use std::ffi::CStr;
use std::os::raw::c_char;
let slice = unsafe { CStr::from_ptr(c_ptr as *const c_char) };
string = slice.to_str().unwrap();
});
panic!("Roc failed with message: {:?}", string)
}
_ => todo!(),
}
}
use std::cell::RefCell;
thread_local! {
pub static MEMORY: RefCell<Option<&'static wasmer::Memory>> = RefCell::new(None);
}
#[allow(dead_code)]
fn fake_wasm_main_function(_: u32, _: u32) -> u32 {
panic!("wasm entered the main function; this should never happen!")
}
#[allow(dead_code)]
pub fn assert_wasm_evals_to_help<T>(src: &str, ignore_problems: bool) -> Result<T, String>
where
T: FromWasmerMemory,
{
let arena = bumpalo::Bump::new();
let context = inkwell::context::Context::create();
let is_gen_test = true;
let instance =
crate::helpers::llvm::helper_wasm(&arena, src, is_gen_test, ignore_problems, &context);
let memory = instance.exports.get_memory("memory").unwrap();
crate::helpers::llvm::MEMORY.with(|f| {
*f.borrow_mut() = Some(unsafe { std::mem::transmute(memory) });
});
let test_wrapper = instance.exports.get_function("test_wrapper").unwrap();
match test_wrapper.call(&[]) {
Err(e) => Err(format!("call to `test_wrapper`: {:?}", e)),
Ok(result) => {
let address = result[0].unwrap_i32();
let output = <T as crate::helpers::llvm::FromWasmerMemory>::decode(
memory,
// skip the RocCallResult tag id
address as u32 + 8,
);
Ok(output)
}
}
}
#[allow(unused_macros)]
macro_rules! assert_wasm_evals_to {
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems:expr) => {
match $crate::helpers::llvm::assert_wasm_evals_to_help::<$ty>($src, $ignore_problems) {
Err(msg) => panic!("Wasm test failed: {:?}", msg),
Ok(actual) => {
#[allow(clippy::bool_assert_comparison)]
assert_eq!($transform(actual), $expected, "Wasm test failed")
}
}
};
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::llvm::assert_wasm_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::llvm::identity,
false
);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::llvm::assert_wasm_evals_to!($src, $expected, $ty, $transform, false);
};
}
#[allow(unused_macros)]
macro_rules! assert_llvm_evals_to {
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems:expr) => {
use bumpalo::Bump;
use inkwell::context::Context;
use roc_gen_llvm::run_jit_function;
let arena = Bump::new();
let context = Context::create();
let is_gen_test = true;
let (main_fn_name, errors, lib) =
$crate::helpers::llvm::helper(&arena, $src, is_gen_test, $ignore_problems, &context);
let transform = |success| {
let expected = $expected;
#[allow(clippy::redundant_closure_call)]
let given = $transform(success);
assert_eq!(&given, &expected, "LLVM test failed");
};
run_jit_function!(lib, main_fn_name, $ty, transform, errors)
};
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::llvm::identity,
false
);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
};
}
#[allow(unused_macros)]
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {{
assert_evals_to!($src, $expected, $ty, $crate::helpers::llvm::identity, false);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{
// same as above, except with an additional transformation argument.
assert_evals_to!($src, $expected, $ty, $transform, false);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{
// same as above, except with ignore_problems.
#[cfg(feature = "wasm-cli-run")]
$crate::helpers::llvm::assert_wasm_evals_to!(
$src,
$expected,
$ty,
$transform,
$ignore_problems
);
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
$expected,
$ty,
$transform,
$ignore_problems
);
}};
}
#[allow(unused_macros)]
macro_rules! assert_expect_failed {
($src:expr, $expected:expr, $ty:ty) => {
use bumpalo::Bump;
use inkwell::context::Context;
use roc_gen_llvm::run_jit_function;
let arena = Bump::new();
let context = Context::create();
let is_gen_test = true;
let (main_fn_name, errors, lib) =
$crate::helpers::llvm::helper(&arena, $src, is_gen_test, false, &context);
let transform = |success| {
let expected = $expected;
assert_eq!(&success, &expected, "LLVM test failed");
};
run_jit_function!(lib, main_fn_name, $ty, transform, errors)
};
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::llvm::identity,
false
);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
};
}
macro_rules! expect_runtime_error_panic {
($src:expr) => {{
#[cfg(feature = "wasm-cli-run")]
$crate::helpers::llvm::assert_wasm_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::llvm::identity,
true // ignore problems
);
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::llvm::identity,
true // ignore problems
);
}};
}
#[allow(dead_code)]
pub fn identity<T>(value: T) -> T {
value
}
#[allow(unused_macros)]
macro_rules! assert_non_opt_evals_to {
($src:expr, $expected:expr, $ty:ty) => {{
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::llvm::identity
);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
// Same as above, except with an additional transformation argument.
{
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform);
}
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform);
}};
}
#[allow(unused_imports)]
pub(crate) use assert_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_expect_failed;
#[allow(unused_imports)]
pub(crate) use assert_llvm_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_non_opt_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_wasm_evals_to;
#[allow(unused_imports)]
pub(crate) use expect_runtime_error_panic;

View file

@ -0,0 +1,65 @@
extern crate bumpalo;
#[cfg(feature = "gen-dev")]
pub mod dev;
pub mod from_wasmer_memory;
#[cfg(feature = "gen-llvm")]
pub mod llvm;
#[cfg(feature = "gen-wasm")]
pub mod wasm;
#[allow(dead_code)]
pub fn zig_executable() -> String {
match std::env::var("ROC_ZIG") {
Ok(path) => path,
Err(_) => "zig".into(),
}
}
/// 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;
/// 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()
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefCount {
Live(u32),
Deallocated,
Constant,
}

View file

@ -0,0 +1,426 @@
use super::RefCount;
use crate::helpers::from_wasmer_memory::FromWasmerMemory;
use roc_collections::all::MutSet;
use roc_gen_wasm::wasm32_result::Wasm32Result;
use roc_gen_wasm::wasm_module::{Export, ExportType};
use roc_gen_wasm::{DEBUG_SETTINGS, MEMORY_NAME};
use roc_load::Threading;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use wasmer::{Memory, WasmPtr};
// Should manually match build.rs
const PLATFORM_FILENAME: &str = "wasm_test_platform";
const OUT_DIR_VAR: &str = "TEST_GEN_OUT";
const TEST_WRAPPER_NAME: &str = "test_wrapper";
const INIT_REFCOUNT_NAME: &str = "init_refcount_test";
const PANIC_MSG_NAME: &str = "panic_msg";
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
}
#[allow(dead_code)]
pub fn compile_and_load<'a, T: Wasm32Result>(
arena: &'a bumpalo::Bump,
src: &str,
test_wrapper_type_info: PhantomData<T>,
) -> wasmer::Instance {
let platform_path = get_preprocessed_host_path();
let platform_bytes = std::fs::read(&platform_path).unwrap();
println!("Loading test host {}", platform_path.display());
let compiled_bytes =
compile_roc_to_wasm_bytes(arena, &platform_bytes, src, test_wrapper_type_info);
if DEBUG_SETTINGS.keep_test_binary {
let build_dir_hash = src_hash(src);
save_wasm_file(&compiled_bytes, build_dir_hash)
};
load_bytes_into_runtime(&compiled_bytes)
}
fn get_preprocessed_host_path() -> PathBuf {
let out_dir = std::env::var(OUT_DIR_VAR).unwrap();
Path::new(&out_dir)
.join([PLATFORM_FILENAME, "o"].join("."))
.to_path_buf()
}
fn src_hash(src: &str) -> u64 {
let mut hash_state = DefaultHasher::new();
src.hash(&mut hash_state);
hash_state.finish()
}
fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>(
arena: &'a bumpalo::Bump,
host_bytes: &[u8],
src: &str,
_test_wrapper_type_info: PhantomData<T>,
) -> Vec<u8> {
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 loaded = roc_load::load_and_monomorphize_from_str(
arena,
filename,
module_src,
src_dir,
Default::default(),
roc_target::TargetInfo::default_wasm32(),
roc_reporting::report::RenderTarget::ColorTerminal,
Threading::Single,
);
let loaded = loaded.expect("failed to load module");
use roc_load::MonomorphizedModule;
let MonomorphizedModule {
module_id,
procedures,
mut interns,
exposed_to_host,
..
} = loaded;
debug_assert_eq!(exposed_to_host.values.len(), 1);
let exposed_to_host = exposed_to_host
.values
.keys()
.copied()
.collect::<MutSet<_>>();
let env = roc_gen_wasm::Env {
arena,
module_id,
exposed_to_host,
};
let host_module = roc_gen_wasm::parse_host(env.arena, host_bytes).unwrap_or_else(|e| {
panic!(
"I ran into a problem with the host object file, {} at offset 0x{:x}:\n{}",
get_preprocessed_host_path().display(),
e.offset,
e.message
)
});
let (mut module, called_preload_fns, main_fn_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
T::insert_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index);
// Export the initialiser function for refcount tests
let init_refcount_idx = module
.names
.function_names
.iter()
.filter(|(_, name)| *name == INIT_REFCOUNT_NAME)
.map(|(i, _)| *i)
.next()
.unwrap();
module.export.append(Export {
name: INIT_REFCOUNT_NAME,
ty: ExportType::Func,
index: init_refcount_idx,
});
module.eliminate_dead_code(env.arena, called_preload_fns);
let mut app_module_bytes = std::vec::Vec::with_capacity(module.size());
module.serialize(&mut app_module_bytes);
app_module_bytes
}
fn save_wasm_file(app_module_bytes: &[u8], build_dir_hash: u64) {
let debug_dir_str = format!("/tmp/roc/gen_wasm/{:016x}", build_dir_hash);
let debug_dir_path = Path::new(&debug_dir_str);
let final_wasm_file = debug_dir_path.join("final.wasm");
std::fs::create_dir_all(debug_dir_path).unwrap();
std::fs::write(&final_wasm_file, app_module_bytes).unwrap();
println!(
"Debug command:\n\twasm-objdump -dx {}",
final_wasm_file.to_str().unwrap()
);
}
fn load_bytes_into_runtime(bytes: &[u8]) -> wasmer::Instance {
use wasmer::{Module, Store};
use wasmer_wasi::WasiState;
let store = Store::default();
let wasmer_module = Module::new(&store, bytes).unwrap();
// First, we create the `WasiEnv`
let mut wasi_env = WasiState::new("hello").finalize().unwrap();
// Then, we get the import object related to our WASI
// and attach it to the Wasm instance.
let import_object = wasi_env
.import_object(&wasmer_module)
.unwrap_or_else(|_| wasmer::imports!());
wasmer::Instance::new(&wasmer_module, &import_object).unwrap()
}
#[allow(dead_code)]
pub fn assert_evals_to_help<T>(src: &str, phantom: PhantomData<T>) -> Result<T, String>
where
T: FromWasmerMemory + Wasm32Result,
{
let arena = bumpalo::Bump::new();
let instance = crate::helpers::wasm::compile_and_load(&arena, src, phantom);
let memory = instance.exports.get_memory(MEMORY_NAME).unwrap();
let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap();
match test_wrapper.call(&[]) {
Err(e) => {
if let Some(msg) = get_roc_panic_msg(&instance, memory) {
Err(format!("Roc failed with message: \"{}\"", msg))
} else {
Err(e.to_string())
}
}
Ok(result) => {
let address = result[0].unwrap_i32();
if false {
println!("test_wrapper returned 0x{:x}", address);
println!("Stack:");
crate::helpers::wasm::debug_memory_hex(memory, address, std::mem::size_of::<T>());
}
if false {
println!("Heap:");
// Manually provide address and size based on printf in wasm_test_platform.c
crate::helpers::wasm::debug_memory_hex(memory, 0x11440, 24);
}
let output = <T as FromWasmerMemory>::decode(memory, address as u32);
Ok(output)
}
}
}
/// Our test roc_panic stores a pointer to its message in a global variable so we can find it.
fn get_roc_panic_msg(instance: &wasmer::Instance, memory: &Memory) -> Option<String> {
let memory_bytes = unsafe { memory.data_unchecked() };
// We need to dereference twice!
// The Wasm Global only points at the memory location of the C global value
let panic_msg_global = instance.exports.get_global(PANIC_MSG_NAME).unwrap();
let global_addr = panic_msg_global.get().unwrap_i32() as usize;
let global_ptr = memory_bytes[global_addr..].as_ptr() as *const u32;
// Dereference again to find the bytes of the message string
let msg_addr = unsafe { *global_ptr };
if msg_addr == 0 {
return None;
}
let msg_index = msg_addr as usize;
let msg_len = memory_bytes[msg_index..]
.iter()
.position(|c| *c == 0)
.unwrap();
let msg_bytes = memory_bytes[msg_index..][..msg_len].to_vec();
let msg = unsafe { String::from_utf8_unchecked(msg_bytes) };
Some(msg)
}
#[allow(dead_code)]
pub fn assert_wasm_refcounts_help<T>(
src: &str,
phantom: PhantomData<T>,
num_refcounts: usize,
) -> Result<Vec<RefCount>, String>
where
T: FromWasmerMemory + Wasm32Result,
{
let arena = bumpalo::Bump::new();
let instance = crate::helpers::wasm::compile_and_load(&arena, src, phantom);
let memory = instance.exports.get_memory(MEMORY_NAME).unwrap();
let expected_len = num_refcounts as i32;
let init_refcount_test = instance.exports.get_function(INIT_REFCOUNT_NAME).unwrap();
let init_result = init_refcount_test.call(&[wasmer::Value::I32(expected_len)]);
let refcount_vector_addr = match init_result {
Err(e) => return Err(format!("{:?}", e)),
Ok(result) => result[0].unwrap_i32(),
};
// Run the test
let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap();
match test_wrapper.call(&[]) {
Err(e) => return Err(format!("{:?}", e)),
Ok(_) => {}
}
// Check we got the right number of refcounts
let refcount_vector_len: WasmPtr<i32> = WasmPtr::new(refcount_vector_addr as u32);
let actual_len = refcount_vector_len.deref(memory).unwrap().get();
if actual_len != expected_len {
panic!("Expected {} refcounts but got {}", expected_len, actual_len);
}
// Read the actual refcount values
let refcount_ptr_array: WasmPtr<WasmPtr<i32>, wasmer::Array> =
WasmPtr::new(4 + refcount_vector_addr as u32);
let refcount_ptrs = refcount_ptr_array
.deref(memory, 0, num_refcounts as u32)
.unwrap();
let mut refcounts = Vec::with_capacity(num_refcounts);
for i in 0..num_refcounts {
let rc_ptr = refcount_ptrs[i].get();
let rc = if rc_ptr.offset() == 0 {
RefCount::Deallocated
} else {
let rc_encoded: i32 = rc_ptr.deref(memory).unwrap().get();
if rc_encoded == 0 {
RefCount::Constant
} else {
let rc = rc_encoded - i32::MIN + 1;
RefCount::Live(rc as u32)
}
};
refcounts.push(rc);
}
Ok(refcounts)
}
/// Print out hex bytes of the test result, and a few words on either side
/// Can be handy for debugging misalignment issues etc.
pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) {
let memory_words: &[u32] = unsafe {
let memory_bytes = memory.data_unchecked();
std::mem::transmute(memory_bytes)
};
let extra_words = 2;
let result_start = (address as usize) / 4;
let result_end = result_start + ((size + 3) / 4);
let start = result_start - extra_words;
let end = result_end + extra_words;
for index in start..end {
let result_marker = if index >= result_start && index < result_end {
"|"
} else {
" "
};
println!(
"{:x} {} {:08x}",
index * 4,
result_marker,
memory_words[index]
);
}
println!();
}
#[allow(unused_macros)]
macro_rules! assert_evals_to {
($src:expr, $expected:expr, $ty:ty) => {
$crate::helpers::wasm::assert_evals_to!(
$src,
$expected,
$ty,
$crate::helpers::wasm::identity,
false
)
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::wasm::assert_evals_to!($src, $expected, $ty, $transform, false);
};
($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{
let phantom = std::marker::PhantomData;
let _ = $ignore_problems; // Always ignore "problems"! One backend (LLVM) is enough to cover them.
match $crate::helpers::wasm::assert_evals_to_help::<$ty>($src, phantom) {
Err(msg) => panic!("{}", msg),
Ok(actual) => {
assert_eq!($transform(actual), $expected)
}
}
}};
}
#[allow(unused_macros)]
macro_rules! expect_runtime_error_panic {
($src:expr) => {{
$crate::helpers::wasm::assert_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::wasm::identity,
true // ignore problems
);
}};
}
#[allow(dead_code)]
pub fn identity<T>(value: T) -> T {
value
}
#[allow(unused_macros)]
macro_rules! assert_refcounts {
// We need the result type to generate the test_wrapper, even though we ignore the value!
// We can't just call `main` with no args, because some tests return structs, via pointer arg!
// Also we need to know how much stack space to reserve for the struct.
($src: expr, $ty: ty, $expected_refcounts: expr) => {{
let phantom = std::marker::PhantomData;
let num_refcounts = $expected_refcounts.len();
let result =
$crate::helpers::wasm::assert_wasm_refcounts_help::<$ty>($src, phantom, num_refcounts);
match result {
Err(msg) => panic!("{:?}", msg),
Ok(actual_refcounts) => {
assert_eq!(&actual_refcounts, $expected_refcounts)
}
}
}};
}
#[allow(unused_imports)]
pub(crate) use assert_evals_to;
#[allow(unused_imports)]
pub(crate) use expect_runtime_error_panic;
#[allow(unused_imports)]
pub(crate) use assert_refcounts;

View file

@ -0,0 +1,26 @@
// Definitions to allow us to run an all-Zig version of the linking test
// Allows us to calculate the expected answer!
extern fn host_called_directly_from_roc() i32;
export var host_result: i32 = 0;
export fn js_called_directly_from_roc() i32 {
return 0x01;
}
export fn js_called_indirectly_from_roc() i32 {
return 0x02;
}
export fn js_called_directly_from_main() i32 {
return 0x04;
}
export fn js_called_indirectly_from_main() i32 {
return 0x08;
}
export fn js_unused() i32 {
return 0x10;
}
export fn roc__app_proc_1_exposed() i32 {
return 0x20 | js_called_directly_from_roc() | host_called_directly_from_roc();
}

View file

@ -0,0 +1,44 @@
extern fn js_called_directly_from_roc() i32;
extern fn js_called_indirectly_from_roc() i32;
extern fn js_called_directly_from_main() i32;
extern fn js_called_indirectly_from_main() i32;
extern fn js_unused() i32;
extern fn roc__app_proc_1_exposed() i32;
export fn host_called_indirectly_from_roc() i32 {
return 0x40;
}
export fn host_called_directly_from_roc() i32 {
return 0x80 | host_called_indirectly_from_roc() | js_called_indirectly_from_roc();
}
export fn host_called_indirectly_from_main() i32 {
return 0x100;
}
export fn host_called_directly_from_main() i32 {
return 0x200 | host_called_indirectly_from_main() | js_called_indirectly_from_main();
}
export fn host_unused() i32 {
// Call some functions from here to get them included in the output file
return 0x400 | js_unused() | js_called_directly_from_roc();
}
// Result is an extern global so the test can read it from the Wasm module
extern var host_result: i32;
pub fn main() !void {
const host = host_called_directly_from_main();
const js = js_called_directly_from_main();
const app = roc__app_proc_1_exposed();
host_result = host | js | app;
if (@import("builtin").target.cpu.arch != .wasm32) {
const stdout = @import("std").io.getStdOut().writer();
try stdout.print("{}\n", .{host_result});
}
}

View file

@ -0,0 +1,152 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Makes test runs take 50% longer, due to linking
#define ENABLE_PRINTF 0
typedef struct
{
size_t length;
size_t *elements[]; // flexible array member
} Vector;
// Globals for refcount testing
Vector *rc_pointers;
size_t rc_pointers_capacity;
// The rust test passes us the max number of allocations it expects to make,
// and we tell it where we're going to write the refcount pointers.
// It won't actually read that memory until later, when the test is done.
Vector *init_refcount_test(size_t capacity)
{
rc_pointers_capacity = capacity;
rc_pointers = malloc((1 + capacity) * sizeof(size_t *));
rc_pointers->length = 0;
for (size_t i = 0; i < capacity; ++i)
rc_pointers->elements[i] = NULL;
return rc_pointers;
}
#if ENABLE_PRINTF
#define ASSERT(condition, format, ...) \
if (!(condition)) \
{ \
printf("ASSERT FAILED: " #format "\n", __VA_ARGS__); \
abort(); \
}
#else
#define ASSERT(condition, format, ...) \
if (!(condition)) \
abort();
#endif
size_t *alloc_ptr_to_rc_ptr(void *ptr, unsigned int alignment)
{
size_t alloc_addr = (size_t)ptr;
size_t rc_addr = alloc_addr + alignment - sizeof(size_t);
return (size_t *)rc_addr;
}
//--------------------------
void *roc_alloc(size_t size, unsigned int alignment)
{
void *allocated = malloc(size);
if (rc_pointers)
{
ASSERT(alignment >= sizeof(size_t), "alignment %zd != %zd", alignment, sizeof(size_t));
size_t num_alloc = rc_pointers->length + 1;
ASSERT(num_alloc <= rc_pointers_capacity, "Too many allocations %zd > %zd", num_alloc, rc_pointers_capacity);
size_t *rc_ptr = alloc_ptr_to_rc_ptr(allocated, alignment);
rc_pointers->elements[rc_pointers->length] = rc_ptr;
rc_pointers->length++;
}
#if ENABLE_PRINTF
if (!allocated)
{
fprintf(stderr, "roc_alloc failed\n");
exit(1);
}
else
{
printf("roc_alloc allocated %d bytes with alignment %d at %p\n", size, alignment, allocated);
}
#endif
return allocated;
}
//--------------------------
void *roc_realloc(void *ptr, size_t new_size, size_t old_size,
unsigned int alignment)
{
#if ENABLE_PRINTF
printf("roc_realloc reallocated %p from %d to %d with alignment %zd\n",
ptr, old_size, new_size, alignment);
#endif
return realloc(ptr, new_size);
}
//--------------------------
void roc_dealloc(void *ptr, unsigned int alignment)
{
if (rc_pointers)
{
// Null out the entry in the test array to indicate that it was freed
// Then even if malloc reuses the space, everything still works
size_t *rc_ptr = alloc_ptr_to_rc_ptr(ptr, alignment);
int i = 0;
for (; i < rc_pointers->length; ++i)
{
if (rc_pointers->elements[i] == rc_ptr)
{
rc_pointers->elements[i] = NULL;
break;
}
}
int was_found = i < rc_pointers->length;
ASSERT(was_found, "RC pointer not found %p", rc_ptr);
}
#if ENABLE_PRINTF
printf("roc_dealloc deallocated %p with alignment %zd\n", ptr, alignment);
#endif
free(ptr);
}
//--------------------------
// Allow the test to probe the panic message
extern char* panic_msg;
void roc_panic(char *msg, unsigned int tag_id)
{
panic_msg = msg;
// Note: no dynamic string formatting
fputs("Application crashed with message\n\n ", stderr);
fputs(msg, stderr);
fputs("\n\nShutting down\n", stderr);
exit(101);
}
//--------------------------
void roc_memcpy(void *dest, const void *src, size_t n)
{
memcpy(dest, src, n);
}
//--------------------------
void *roc_memset(void *str, int c, size_t n)
{
return memset(str, c, n);
}

View file

@ -0,0 +1,78 @@
#![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)]
pub mod gen_abilities;
pub mod gen_compare;
pub mod gen_dict;
pub mod gen_list;
pub mod gen_num;
pub mod gen_primitives;
pub mod gen_records;
pub mod gen_refcount;
pub mod gen_result;
pub mod gen_set;
pub mod gen_str;
pub mod gen_tags;
mod helpers;
pub mod wasm_str;
#[cfg(feature = "gen-wasm")]
pub mod wasm_linking;
use core::ffi::c_void;
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
libc::malloc(size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_memcpy(dest: *mut c_void, src: *const c_void, bytes: usize) -> *mut c_void {
libc::memcpy(dest, src, bytes)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
libc::realloc(c_ptr, new_size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
libc::free(c_ptr)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
use roc_gen_llvm::llvm::build::PanicTagId;
use std::ffi::CStr;
use std::os::raw::c_char;
match PanicTagId::try_from(tag_id) {
Ok(PanicTagId::NullTerminatedString) => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
Err(_) => unreachable!(),
}
}

View file

@ -0,0 +1,373 @@
#![cfg(feature = "gen-wasm")]
use bumpalo::Bump;
use roc_gen_wasm::Env;
use std::fs;
use std::process::Command;
use roc_builtins::bitcode::IntWidth;
use roc_collections::{MutMap, MutSet};
use roc_gen_wasm::wasm_module::WasmModule;
use roc_module::ident::{ForeignSymbol, ModuleName};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{
IdentIds, IdentIdsByModule, Interns, ModuleId, ModuleIds, PackageModuleIds, PackageQualified,
Symbol,
};
use roc_mono::ir::{
Call, CallType, Expr, HostExposedLayouts, Literal, Proc, ProcLayout, SelfRecursive, Stmt,
UpdateModeId,
};
use roc_mono::layout::{Builtin, Layout};
const LINKING_TEST_HOST_WASM: &str = "build/wasm_linking_test_host.wasm";
const LINKING_TEST_HOST_NATIVE: &str = "build/wasm_linking_test_host";
fn create_symbol(home: ModuleId, ident_ids: &mut IdentIds, debug_name: &str) -> Symbol {
let ident_id = ident_ids.add_str(debug_name);
Symbol::new(home, ident_id)
}
// Build a fake Roc app in mono IR
// Calls two host functions, one Wasm and one JS
fn build_app_mono<'a>(
arena: &'a Bump,
home: ModuleId,
ident_ids: &mut IdentIds,
) -> (Symbol, MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>) {
let int_layout = Layout::Builtin(Builtin::Int(IntWidth::I32));
let int_layout_ref = arena.alloc(int_layout);
let app_proc = create_symbol(home, ident_ids, "app_proc");
let js_call_result = create_symbol(home, ident_ids, "js_call_result");
let host_call_result = create_symbol(home, ident_ids, "host_call_result");
let bitflag = create_symbol(home, ident_ids, "bitflag");
let or1 = create_symbol(home, ident_ids, "or1");
let or2 = create_symbol(home, ident_ids, "or2");
let js_call = Expr::Call(Call {
call_type: CallType::Foreign {
foreign_symbol: ForeignSymbol::from("js_called_directly_from_roc"),
ret_layout: int_layout_ref,
},
arguments: &[],
});
let host_call = Expr::Call(Call {
call_type: CallType::Foreign {
foreign_symbol: ForeignSymbol::from("host_called_directly_from_roc"),
ret_layout: int_layout_ref,
},
arguments: &[],
});
let mut bitflag_bytes = [0; 16];
bitflag_bytes[0] = 0x20;
let bitflag_literal = Expr::Literal(Literal::Int(bitflag_bytes));
let or1_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::Or,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: arena.alloc([js_call_result, host_call_result]),
});
let or2_expr = Expr::Call(Call {
call_type: CallType::LowLevel {
op: LowLevel::Or,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: arena.alloc([or1, bitflag]),
});
let body = Stmt::Let(
js_call_result,
js_call,
int_layout,
arena.alloc(Stmt::Let(
host_call_result,
host_call,
int_layout,
arena.alloc(Stmt::Let(
or1,
or1_expr,
int_layout,
arena.alloc(Stmt::Let(
bitflag,
bitflag_literal,
int_layout,
arena.alloc(Stmt::Let(
or2,
or2_expr,
int_layout,
//
arena.alloc(Stmt::Ret(or2)),
)),
)),
)),
)),
);
let proc = Proc {
name: app_proc,
args: &[],
body,
closure_data_layout: None,
ret_layout: int_layout,
is_self_recursive: SelfRecursive::NotSelfRecursive,
must_own_arguments: false,
host_exposed_layouts: HostExposedLayouts::NotHostExposed,
};
let proc_layout = ProcLayout {
arguments: &[],
result: int_layout,
};
let mut app = MutMap::default();
app.insert((app_proc, proc_layout), proc);
(app_proc, app)
}
struct BackendInputs<'a> {
env: Env<'a>,
interns: Interns,
host_module: WasmModule<'a>,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
}
impl<'a> BackendInputs<'a> {
fn new(arena: &'a Bump) -> Self {
// Compile the host from an external source file
let host_bytes = fs::read(LINKING_TEST_HOST_WASM).unwrap();
let host_module: WasmModule = roc_gen_wasm::parse_host(arena, &host_bytes).unwrap();
// Identifier stuff to build the mono IR
let module_name = ModuleName::from("UserApp");
let pkg_qualified_module_name = PackageQualified::Unqualified(module_name);
let mut package_module_ids = PackageModuleIds::default();
let module_id: ModuleId = package_module_ids.get_or_insert(&pkg_qualified_module_name);
let mut ident_ids = IdentIds::default();
// IR for the app
let (roc_main_sym, procedures) = build_app_mono(arena, module_id, &mut ident_ids);
let mut exposed_to_host = MutSet::default();
exposed_to_host.insert(roc_main_sym);
let env = Env {
arena,
module_id,
exposed_to_host,
};
// Identifier stuff for the backend
let module_ids = ModuleIds::default();
let mut all_ident_ids: IdentIdsByModule = IdentIds::exposed_builtins(1);
all_ident_ids.insert(module_id, ident_ids);
let interns = Interns {
module_ids,
all_ident_ids,
};
BackendInputs {
env,
interns,
host_module,
procedures,
}
}
}
fn execute_wasm_bytes(bytes: &[u8]) -> Result<wasmer::Instance, String> {
use wasmer::{Function, Module, Store};
let store = Store::default();
let wasmer_module = Module::new(&store, bytes).map_err(|e| e.to_string())?;
let import_object = wasmer::imports!(
"env" => {
"js_called_indirectly_from_roc" => Function::new_native(&store, js_called_indirectly_from_roc),
"js_called_indirectly_from_main" => Function::new_native(&store, js_called_indirectly_from_main),
"js_unused" => Function::new_native(&store, js_unused),
"js_called_directly_from_roc" => Function::new_native(&store, js_called_directly_from_roc),
"js_called_directly_from_main" => Function::new_native(&store, js_called_directly_from_main),
}
);
let instance =
wasmer::Instance::new(&wasmer_module, &import_object).map_err(|e| e.to_string())?;
let start = instance
.exports
.get_function("_start")
.map_err(|e| e.to_string())?;
start.call(&[]).map_err(|e| e.to_string())?;
Ok(instance)
}
fn js_called_directly_from_roc() -> i32 {
return 0x01;
}
fn js_called_indirectly_from_roc() -> i32 {
return 0x02;
}
fn js_called_directly_from_main() -> i32 {
return 0x04;
}
fn js_called_indirectly_from_main() -> i32 {
return 0x08;
}
fn js_unused() -> i32 {
return 0x10;
}
fn get_wasm_result(instance: &wasmer::Instance) -> i32 {
let memory = instance
.exports
.get_memory(roc_gen_wasm::MEMORY_NAME)
.unwrap();
let memory_bytes = unsafe { memory.data_unchecked() };
// The Wasm Global tells us the memory location of the global value
let global = instance.exports.get_global("host_result").unwrap();
let global_addr = global.get().unwrap_i32() as usize;
let mut global_bytes = [0; 4];
global_bytes.copy_from_slice(&memory_bytes[global_addr..][..4]);
i32::from_le_bytes(global_bytes)
}
fn get_native_result() -> i32 {
let output = Command::new(LINKING_TEST_HOST_NATIVE)
.output()
.expect(&format!("failed to run {}", LINKING_TEST_HOST_NATIVE));
let result_str = std::str::from_utf8(&output.stdout).unwrap().trim();
result_str.parse().unwrap()
}
#[test]
fn test_linking_without_dce() {
let arena = Bump::new();
let BackendInputs {
env,
mut interns,
host_module,
procedures,
} = BackendInputs::new(&arena);
let host_import_names = Vec::from_iter(host_module.import.imports.iter().map(|i| i.name));
assert_eq!(
&host_import_names,
&[
"__linear_memory",
"__stack_pointer",
"js_called_indirectly_from_roc",
"js_called_indirectly_from_main",
"js_unused",
"js_called_directly_from_roc",
"js_called_directly_from_main",
"roc__app_proc_1_exposed",
"__indirect_function_table",
]
);
let (final_module, _called_preload_fns, _roc_main_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
let mut buffer = Vec::with_capacity(final_module.size());
final_module.serialize(&mut buffer);
if std::env::var("DEBUG_WASM").is_ok() {
fs::write("build/without_dce.wasm", &buffer).unwrap();
}
let final_import_names = Vec::from_iter(final_module.import.imports.iter().map(|i| i.name));
assert_eq!(
&final_import_names,
&[
"js_called_indirectly_from_roc",
"js_called_indirectly_from_main",
"js_unused",
"js_called_directly_from_roc",
"js_called_directly_from_main",
]
);
let instance = execute_wasm_bytes(&buffer).unwrap();
let wasm_result = get_wasm_result(&instance);
assert_eq!(wasm_result, get_native_result());
}
#[test]
fn test_linking_with_dce() {
let arena = Bump::new();
let BackendInputs {
env,
mut interns,
host_module,
procedures,
} = BackendInputs::new(&arena);
let host_import_names = Vec::from_iter(host_module.import.imports.iter().map(|imp| imp.name));
assert_eq!(
&host_import_names,
&[
"__linear_memory",
"__stack_pointer",
"js_called_indirectly_from_roc",
"js_called_indirectly_from_main",
"js_unused",
"js_called_directly_from_roc",
"js_called_directly_from_main",
"roc__app_proc_1_exposed",
"__indirect_function_table",
]
);
assert!(&host_module.names.function_names.is_empty());
let (mut final_module, called_preload_fns, _roc_main_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
final_module.eliminate_dead_code(env.arena, called_preload_fns);
let mut buffer = Vec::with_capacity(final_module.size());
final_module.serialize(&mut buffer);
if std::env::var("DEBUG_WASM").is_ok() {
fs::write("build/with_dce.wasm", &buffer).unwrap();
}
let final_import_names = Vec::from_iter(final_module.import.imports.iter().map(|i| i.name));
assert_eq!(
&final_import_names,
&[
"js_called_indirectly_from_roc",
"js_called_indirectly_from_main",
"js_called_directly_from_roc",
"js_called_directly_from_main",
]
);
assert_eq!(
&final_module.names.function_names[0..5],
&[
(0, "js_called_indirectly_from_roc"),
(1, "js_called_indirectly_from_main"),
(2, "js_called_directly_from_roc"),
(3, "js_called_directly_from_main"),
(4, "js_unused"),
]
);
let instance = execute_wasm_bytes(&buffer).unwrap();
let wasm_result = get_wasm_result(&instance);
assert_eq!(wasm_result, get_native_result());
}

File diff suppressed because it is too large Load diff