diff --git a/Cargo.lock b/Cargo.lock index d473e23664..fbeeccc742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" dependencies = [ - "gimli 0.26.1", + "gimli", ] [[package]] @@ -739,66 +739,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ca3560686e7c9c7ed7e0fe77469f2410ba5d7781b1acaa9adc8d8deea28e3e" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf9bf1ffffb6ce3d2e5ebc83549bd2436426c99b31cc550d521364cbe35d276" -dependencies = [ - "cranelift-bforest", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-entity", - "gimli 0.24.0", - "log", - "regalloc", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc21936a5a6d07e23849ffe83e5c1f6f50305c074f4b2970ca50c13bf55b821" -dependencies = [ - "cranelift-codegen-shared", - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5b6ffaa87560bebe69a5446449da18090b126037920b0c1c6d5945f72faf6b" - -[[package]] -name = "cranelift-entity" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6b4a8bef04f82e4296782646f733c641d09497df2fabf791323fefaa44c64c" - -[[package]] -name = "cranelift-frontend" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b783b351f966fce33e3c03498cb116d16d97a8f9978164a60920bd0d3a99c" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - [[package]] name = "crc32fast" version = "1.2.1" @@ -1193,6 +1133,32 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dynasm" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b1801e630bd336d0bbbdbf814de6cc749c9a400c7e3d995e6adfd455d0c83c" +dependencies = [ + "bitflags", + "byteorder", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dynasmrt" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d428afc93ad288f6dffc1fa5f4a78201ad2eec33c5a522e51c181009eb09061" +dependencies = [ + "byteorder", + "dynasm", + "memmap2 0.5.0", +] + [[package]] name = "either" version = "1.6.1" @@ -1280,12 +1246,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fd-lock" version = "3.0.2" @@ -1505,17 +1465,6 @@ dependencies = [ "syn", ] -[[package]] -name = "gimli" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.26.1" @@ -3124,17 +3073,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "regalloc" -version = "0.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571f7f397d61c4755285cd37853fe8e03271c243424a907415909379659381c5" -dependencies = [ - "log", - "rustc-hash", - "smallvec", -] - [[package]] name = "regex" version = "1.5.4" @@ -3194,6 +3132,8 @@ dependencies = [ "roc_repl_cli", "roc_test_utils", "strip-ansi-escapes", + "wasmer", + "wasmer-wasi", ] [[package]] @@ -3705,6 +3645,7 @@ name = "roc_repl_wasm" version = "0.1.0" dependencies = [ "bumpalo", + "futures", "js-sys", "roc_builtins", "roc_collections", @@ -4213,12 +4154,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -4746,7 +4681,7 @@ dependencies = [ "target-lexicon", "thiserror", "wasmer-compiler", - "wasmer-compiler-cranelift", + "wasmer-compiler-singlepass", "wasmer-derive", "wasmer-engine", "wasmer-engine-dylib", @@ -4776,20 +4711,19 @@ dependencies = [ ] [[package]] -name = "wasmer-compiler-cranelift" +name = "wasmer-compiler-singlepass" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a570746cbec434179e2d53357973a34dfdb208043104e8fac3b7b0023015cf6" +checksum = "9429b9f7708c582d855b1787f09c7029ff23fb692550d4a1cc351c8ea84c3014" dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "gimli 0.24.0", + "byteorder", + "dynasm", + "dynasmrt", + "lazy_static", "loupe", "more-asserts", "rayon", "smallvec", - "tracing", "wasmer-compiler", "wasmer-types", "wasmer-vm", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index dfffe25ee0..d47d8cce8a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -66,11 +66,11 @@ mimalloc = { version = "0.1.26", default-features = false } target-lexicon = "0.12.2" tempfile = "3.2.0" -wasmer = { version = "2.0.0", optional = true, default-features = false, features = ["default-cranelift", "default-universal"] } +wasmer = { version = "2.0.0", optional = true, default-features = false, features = ["default-singlepass", "default-universal"] } wasmer-wasi = { version = "2.0.0", optional = true } [dev-dependencies] -wasmer = { version = "2.0.0", default-features = false, features = ["default-cranelift", "default-universal"] } +wasmer = { version = "2.0.0", default-features = false, features = ["default-singlepass", "default-universal"] } wasmer-wasi = "2.0.0" pretty_assertions = "1.0.0" roc_test_utils = { path = "../test_utils" } diff --git a/cli_utils/Cargo.lock b/cli_utils/Cargo.lock index c25e9072c9..c385582180 100644 --- a/cli_utils/Cargo.lock +++ b/cli_utils/Cargo.lock @@ -2771,7 +2771,6 @@ dependencies = [ "roc_types", "roc_unify", "ven_pretty", - "wasm-bindgen", ] [[package]] @@ -2880,7 +2879,6 @@ dependencies = [ "roc_reporting", "roc_target", "roc_types", - "wasm-bindgen", ] [[package]] @@ -2913,7 +2911,6 @@ dependencies = [ "roc_region", "roc_types", "roc_unify", - "wasm-bindgen", ] [[package]] diff --git a/compiler/test_gen/Cargo.toml b/compiler/test_gen/Cargo.toml index 607b14b7ff..53e7e968c0 100644 --- a/compiler/test_gen/Cargo.toml +++ b/compiler/test_gen/Cargo.toml @@ -39,7 +39,7 @@ libc = "0.2.106" inkwell = { path = "../../vendor/inkwell" } target-lexicon = "0.12.2" libloading = "0.7.1" -wasmer = { version = "2.0.0", default-features = false, features = ["default-cranelift", "default-universal"] } +wasmer = { version = "2.0.0", default-features = false, features = ["default-singlepass", "default-universal"] } wasmer-wasi = "2.0.0" tempfile = "3.2.0" indoc = "1.0.3" diff --git a/compiler/test_gen/src/helpers/llvm.rs b/compiler/test_gen/src/helpers/llvm.rs index 24569e3906..29854fbcde 100644 --- a/compiler/test_gen/src/helpers/llvm.rs +++ b/compiler/test_gen/src/helpers/llvm.rs @@ -489,10 +489,7 @@ where match test_wrapper.call(&[]) { Err(e) => Err(format!("call to `test_wrapper`: {:?}", e)), Ok(result) => { - let address = match result[0] { - wasmer::Value::I32(a) => a, - _ => panic!(), - }; + let address = result[0].unwrap_i32(); let output = ::decode( memory, diff --git a/compiler/test_gen/src/helpers/wasm.rs b/compiler/test_gen/src/helpers/wasm.rs index 3420cb2498..6813459933 100644 --- a/compiler/test_gen/src/helpers/wasm.rs +++ b/compiler/test_gen/src/helpers/wasm.rs @@ -194,10 +194,7 @@ where match test_wrapper.call(&[]) { Err(e) => Err(format!("{:?}", e)), Ok(result) => { - let address = match result[0] { - wasmer::Value::I32(a) => a, - _ => panic!(), - }; + let address = result[0].unwrap_i32(); if false { println!("test_wrapper returned 0x{:x}", address); @@ -239,10 +236,7 @@ where 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) => match result[0] { - wasmer::Value::I32(a) => a, - _ => panic!(), - }, + Ok(result) => result[0].unwrap_i32(), }; // Run the test diff --git a/repl_test/Cargo.toml b/repl_test/Cargo.toml index 2d86ad19d4..4bd8992b6d 100644 --- a/repl_test/Cargo.toml +++ b/repl_test/Cargo.toml @@ -12,13 +12,12 @@ roc_cli = {path = "../cli"} [dev-dependencies] indoc = "1.0.3" -roc_test_utils = {path = "../test_utils"} strip-ansi-escapes = "0.1.1" +wasmer = {version = "2.0.0", default-features = false, features = ["default-singlepass", "default-universal"]} +wasmer-wasi = "2.0.0" roc_repl_cli = {path = "../repl_cli"} -# roc_repl_wasm = {path = "../repl_wasm"} +roc_test_utils = {path = "../test_utils"} [features] -default = ["cli"] -cli = [] wasm = [] diff --git a/repl_test/run_wasm.sh b/repl_test/run_wasm.sh new file mode 100755 index 0000000000..4d5ffbce3c --- /dev/null +++ b/repl_test/run_wasm.sh @@ -0,0 +1,6 @@ +# At the moment we are using this script instead of `cargo test` +# Cargo doesn't really have a good way to build two targets (host and wasm). +# We can try to make the build nicer later + +cargo build --target wasm32-unknown-unknown -p roc_repl_wasm --features wasmer --release +cargo test -p repl_test --features wasm diff --git a/repl_test/src/tests.rs b/repl_test/src/tests.rs index 49a166386b..d6e88e44c3 100644 --- a/repl_test/src/tests.rs +++ b/repl_test/src/tests.rs @@ -1,10 +1,16 @@ use indoc::indoc; +#[cfg(not(feature = "wasm"))] mod cli; - -#[cfg(feature = "cli")] +#[cfg(not(feature = "wasm"))] use crate::cli::{expect_failure, expect_success}; +#[cfg(feature = "wasm")] +mod wasm; +#[cfg(feature = "wasm")] +#[allow(unused_imports)] +use crate::wasm::{expect_failure, expect_success}; + #[test] fn literal_0() { expect_success("0", "0 : Num *"); @@ -50,16 +56,19 @@ fn float_addition() { expect_success("1.1 + 2", "3.1 : F64"); } +#[cfg(not(feature = "wasm"))] #[test] fn num_rem() { expect_success("299 % 10", "Ok 9 : Result (Int *) [ DivByZero ]*"); } +#[cfg(not(feature = "wasm"))] #[test] fn num_floor_division_success() { expect_success("Num.divFloor 4 3", "Ok 1 : Result (Int *) [ DivByZero ]*"); } +#[cfg(not(feature = "wasm"))] #[test] fn num_floor_division_divby_zero() { expect_success( @@ -68,11 +77,13 @@ fn num_floor_division_divby_zero() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn num_ceil_division_success() { expect_success("Num.divCeil 4 3", "Ok 2 : Result (Int *) [ DivByZero ]*") } +#[cfg(not(feature = "wasm"))] #[test] fn bool_in_record() { expect_success("{ x: 1 == 1 }", "{ x: True } : { x : Bool }"); @@ -87,18 +98,21 @@ fn bool_in_record() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn bool_basic_equality() { expect_success("1 == 1", "True : Bool"); expect_success("1 != 1", "False : Bool"); } +#[cfg(not(feature = "wasm"))] #[test] fn arbitrary_tag_unions() { expect_success("if 1 == 1 then Red else Green", "Red : [ Green, Red ]*"); expect_success("if 1 != 1 then Red else Green", "Green : [ Green, Red ]*"); } +#[cfg(not(feature = "wasm"))] #[test] fn tag_without_arguments() { expect_success("True", "True : [ True ]*"); @@ -118,6 +132,7 @@ fn byte_tag_union() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn tag_in_record() { expect_success( @@ -137,11 +152,13 @@ fn single_element_tag_union() { expect_success("Foo 1 3.14", "Foo 1 3.14 : [ Foo (Num *) (Float *) ]*"); } +#[cfg(not(feature = "wasm"))] #[test] fn newtype_of_unit() { expect_success("Foo Bar", "Foo Bar : [ Foo [ Bar ]* ]*"); } +#[cfg(not(feature = "wasm"))] #[test] fn newtype_of_big_data() { expect_success( @@ -157,6 +174,7 @@ fn newtype_of_big_data() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn newtype_nested() { expect_success( @@ -172,6 +190,7 @@ fn newtype_nested() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn newtype_of_big_of_newtype() { expect_success( @@ -202,16 +221,19 @@ fn literal_empty_str() { expect_success("\"\"", "\"\" : Str"); } +#[cfg(not(feature = "wasm"))] #[test] fn literal_ascii_str() { expect_success("\"Hello, World!\"", "\"Hello, World!\" : Str"); } +#[cfg(not(feature = "wasm"))] #[test] fn literal_utf8_str() { expect_success("\"👩‍👩‍👦‍👦\"", "\"👩‍👩‍👦‍👦\" : Str"); } +#[cfg(not(feature = "wasm"))] #[test] fn str_concat() { expect_success( @@ -230,6 +252,7 @@ fn literal_empty_list() { expect_success("[]", "[] : List *"); } +#[cfg(not(feature = "wasm"))] #[test] fn literal_empty_list_empty_record() { expect_success("[ {} ]", "[ {} ] : List {}"); @@ -250,11 +273,13 @@ fn literal_float_list() { expect_success("[ 1.1, 2.2, 3.3 ]", "[ 1.1, 2.2, 3.3 ] : List (Float *)"); } +#[cfg(not(feature = "wasm"))] #[test] fn literal_string_list() { expect_success(r#"[ "a", "b", "cd" ]"#, r#"[ "a", "b", "cd" ] : List Str"#); } +#[cfg(not(feature = "wasm"))] #[test] fn nested_string_list() { expect_success( @@ -322,6 +347,7 @@ fn num_mul_wrap() { expect_success("Num.mulWrap Num.maxI64 2", "-2 : I64"); } +#[cfg(not(feature = "wasm"))] #[test] fn num_add_checked() { expect_success("Num.addChecked 1 1", "Ok 2 : Result (Num *) [ Overflow ]*"); @@ -331,6 +357,7 @@ fn num_add_checked() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn num_sub_checked() { expect_success("Num.subChecked 1 1", "Ok 0 : Result (Num *) [ Overflow ]*"); @@ -340,6 +367,7 @@ fn num_sub_checked() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn num_mul_checked() { expect_success( @@ -352,6 +380,7 @@ fn num_mul_checked() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn list_concat() { expect_success( @@ -360,6 +389,7 @@ fn list_concat() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn list_contains() { expect_success("List.contains [] 0", "False : Bool"); @@ -367,6 +397,7 @@ fn list_contains() { expect_success("List.contains [ 1, 2, 3 ] 4", "False : Bool"); } +#[cfg(not(feature = "wasm"))] #[test] fn list_sum() { expect_success("List.sum []", "0 : Num *"); @@ -374,6 +405,7 @@ fn list_sum() { expect_success("List.sum [ 1.1, 2.2, 3.3 ]", "6.6 : F64"); } +#[cfg(not(feature = "wasm"))] #[test] fn list_first() { expect_success( @@ -386,6 +418,7 @@ fn list_first() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn list_last() { expect_success( @@ -399,6 +432,7 @@ fn list_last() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn empty_record() { expect_success("{}", "{} : {}"); @@ -522,16 +556,19 @@ fn identity_lambda() { expect_success("\\x -> x", " : a -> a"); } +#[cfg(not(feature = "wasm"))] #[test] fn sum_lambda() { expect_success("\\x, y -> x + y", " : Num a, Num a -> Num a"); } +#[cfg(not(feature = "wasm"))] #[test] fn stdlib_function() { expect_success("Num.abs", " : Num a -> Num a"); } +#[cfg(not(feature = "wasm"))] #[test] fn too_few_args() { expect_failure( @@ -552,6 +589,7 @@ fn too_few_args() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn type_problem() { expect_failure( @@ -608,6 +646,7 @@ fn multiline_input() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn recursive_tag_union_flat_variant() { expect_success( @@ -623,6 +662,7 @@ fn recursive_tag_union_flat_variant() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn large_recursive_tag_union_flat_variant() { expect_success( @@ -639,6 +679,7 @@ fn large_recursive_tag_union_flat_variant() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn recursive_tag_union_recursive_variant() { expect_success( @@ -654,6 +695,7 @@ fn recursive_tag_union_recursive_variant() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn large_recursive_tag_union_recursive_variant() { expect_success( @@ -670,6 +712,7 @@ fn large_recursive_tag_union_recursive_variant() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn recursive_tag_union_into_flat_tag_union() { expect_success( @@ -685,6 +728,7 @@ fn recursive_tag_union_into_flat_tag_union() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn non_nullable_unwrapped_tag_union() { expect_success( @@ -704,6 +748,7 @@ fn non_nullable_unwrapped_tag_union() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn nullable_unwrapped_tag_union() { expect_success( @@ -723,6 +768,7 @@ fn nullable_unwrapped_tag_union() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn nullable_wrapped_tag_union() { expect_success( @@ -746,6 +792,7 @@ fn nullable_wrapped_tag_union() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn large_nullable_wrapped_tag_union() { // > 7 non-empty variants so that to force tag storage alongside the data @@ -770,6 +817,7 @@ fn large_nullable_wrapped_tag_union() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn issue_2300() { expect_success( @@ -778,6 +826,7 @@ fn issue_2300() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn function_in_list() { expect_success( @@ -786,6 +835,7 @@ fn function_in_list() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn function_in_record() { expect_success( @@ -794,6 +844,7 @@ fn function_in_record() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn function_in_unwrapped_record() { expect_success( @@ -802,6 +853,7 @@ fn function_in_unwrapped_record() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn function_in_tag() { expect_success( @@ -832,6 +884,7 @@ fn print_u8s() { ) } +#[cfg(not(feature = "wasm"))] #[test] fn parse_problem() { expect_failure( @@ -855,6 +908,7 @@ fn parse_problem() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn mono_problem() { expect_failure( @@ -887,6 +941,7 @@ fn mono_problem() { ); } +#[cfg(not(feature = "wasm"))] #[test] fn issue_2343_complete_mono_with_shadowed_vars() { expect_failure( diff --git a/repl_test/src/wasm.rs b/repl_test/src/wasm.rs new file mode 100644 index 0000000000..a6f0a86a99 --- /dev/null +++ b/repl_test/src/wasm.rs @@ -0,0 +1,261 @@ +use std::{ + cell::RefCell, + fs, + ops::{Deref, DerefMut}, + path::Path, + thread_local, +}; +use wasmer::{imports, Function, Instance, Module, Store, Value}; +use wasmer_wasi::WasiState; + +const WASM_REPL_COMPILER_PATH: &str = "../target/wasm32-unknown-unknown/release/roc_repl_wasm.wasm"; + +thread_local! { + static COMPILER: RefCell> = RefCell::new(None) +} + +thread_local! { + static REPL_STATE: RefCell> = RefCell::new(None) +} + +struct ReplState { + src: &'static str, + app: Option, + result_addr: Option, + output: Option, +} + +fn wasmer_create_app(app_bytes_ptr: u32, app_bytes_len: u32) { + let app = COMPILER.with(|f| { + if let Some(compiler) = f.borrow().deref() { + let memory = compiler.exports.get_memory("memory").unwrap(); + let memory_bytes: &[u8] = unsafe { memory.data_unchecked() }; + + // Find the slice of bytes for the compiled Roc app + let ptr = app_bytes_ptr as usize; + let len = app_bytes_len as usize; + let app_module_bytes: &[u8] = &memory_bytes[ptr..][..len]; + + // Parse the bytes into a Wasmer module + let store = Store::default(); + let wasmer_module = Module::new(&store, app_module_bytes).unwrap(); + + // Get the WASI imports for the app + let mut wasi_env = WasiState::new("hello").finalize().unwrap(); + let import_object = wasi_env + .import_object(&wasmer_module) + .unwrap_or_else(|_| imports!()); + + // Create an executable instance. (Give it a stack & heap, etc. If this was ELF, it would be the OS's job.) + Instance::new(&wasmer_module, &import_object).unwrap() + } else { + unreachable!() + } + }); + + REPL_STATE.with(|f| { + if let Some(state) = f.borrow_mut().deref_mut() { + state.app = Some(app) + } else { + unreachable!() + } + }); +} + +fn wasmer_run_app() -> u32 { + REPL_STATE.with(|f| { + if let Some(state) = f.borrow_mut().deref_mut() { + if let Some(app) = &state.app { + let wrapper = app.exports.get_function("wrapper").unwrap(); + + let result_addr: i32 = match wrapper.call(&[]) { + Err(e) => panic!("{:?}", e), + Ok(result) => result[0].unwrap_i32(), + }; + state.result_addr = Some(result_addr as u32); + + let memory = app.exports.get_memory("memory").unwrap(); + memory.size().bytes().0 as u32 + } else { + unreachable!() + } + } else { + unreachable!() + } + }) +} + +fn wasmer_get_result_and_memory(buffer_alloc_addr: u32) -> u32 { + REPL_STATE.with(|f| { + if let Some(state) = f.borrow().deref() { + if let Some(app) = &state.app { + let app_memory = app.exports.get_memory("memory").unwrap(); + let result_addr = state.result_addr.unwrap(); + + let app_memory_bytes: &[u8] = unsafe { app_memory.data_unchecked() }; + + let buf_addr = buffer_alloc_addr as usize; + let len = app_memory_bytes.len(); + + COMPILER.with(|f| { + if let Some(compiler) = f.borrow().deref() { + let memory = compiler.exports.get_memory("memory").unwrap(); + let compiler_memory_bytes: &mut [u8] = + unsafe { memory.data_unchecked_mut() }; + compiler_memory_bytes[buf_addr..][..len].copy_from_slice(app_memory_bytes); + } else { + unreachable!() + } + }); + + result_addr + } else { + panic!("REPL app not found") + } + } else { + panic!("REPL state not found") + } + }) +} + +fn wasmer_copy_input_string(src_buffer_addr: u32) { + let src = REPL_STATE.with(|rs| { + if let Some(state) = rs.borrow_mut().deref_mut() { + std::mem::take(&mut state.src) + } else { + unreachable!() + } + }); + + COMPILER.with(|c| { + if let Some(compiler) = c.borrow().deref() { + let memory = compiler.exports.get_memory("memory").unwrap(); + let memory_bytes: &mut [u8] = unsafe { memory.data_unchecked_mut() }; + + let buf_addr = src_buffer_addr as usize; + let len = src.len(); + memory_bytes[buf_addr..][..len].copy_from_slice(src.as_bytes()); + } else { + unreachable!() + } + }) +} + +fn wasmer_copy_output_string(output_ptr: u32, output_len: u32) { + let output: String = COMPILER.with(|c| { + if let Some(compiler) = c.borrow().deref() { + let memory = compiler.exports.get_memory("memory").unwrap(); + let memory_bytes: &[u8] = unsafe { memory.data_unchecked() }; + + // Find the slice of bytes for the output string + let ptr = output_ptr as usize; + let len = output_len as usize; + let output_bytes: &[u8] = &memory_bytes[ptr..][..len]; + + // Copy it out of the Wasm module + let copied_bytes = output_bytes.to_vec(); + unsafe { String::from_utf8_unchecked(copied_bytes) } + } else { + unreachable!() + } + }); + + REPL_STATE.with(|f| { + if let Some(state) = f.borrow_mut().deref_mut() { + state.output = Some(output) + } + }) +} + +fn init_compiler() -> Instance { + let path = Path::new(WASM_REPL_COMPILER_PATH); + let wasm_module_bytes = match fs::read(&path) { + Ok(bytes) => bytes, + Err(e) => panic!("{}", format_compiler_load_error(e)), + }; + + let store = Store::default(); + let wasmer_module = Module::new(&store, &wasm_module_bytes).unwrap(); + + let import_object = imports! { + "env" => { + "wasmer_create_app" => Function::new_native(&store, wasmer_create_app), + "wasmer_run_app" => Function::new_native(&store, wasmer_run_app), + "wasmer_get_result_and_memory" => Function::new_native(&store, wasmer_get_result_and_memory), + "wasmer_copy_input_string" => Function::new_native(&store, wasmer_copy_input_string), + "wasmer_copy_output_string" => Function::new_native(&store, wasmer_copy_output_string), + "now" => Function::new_native(&store, dummy_system_time_now), + } + }; + + Instance::new(&wasmer_module, &import_object).unwrap() +} + +fn run(src: &'static str) -> (bool, String) { + REPL_STATE.with(|rs| { + *rs.borrow_mut().deref_mut() = Some(ReplState { + src, + app: None, + result_addr: None, + output: None, + }); + }); + + let ok = COMPILER.with(|c| { + *c.borrow_mut().deref_mut() = Some(init_compiler()); + + if let Some(compiler) = c.borrow().deref() { + let entrypoint = compiler + .exports + .get_function("entrypoint_from_test") + .unwrap(); + + let src_len = Value::I32(src.len() as i32); + let wasm_ok: i32 = entrypoint.call(&[src_len]).unwrap().deref()[0].unwrap_i32(); + wasm_ok != 0 + } else { + unreachable!() + } + }); + + let final_state: ReplState = REPL_STATE.with(|rs| rs.take()).unwrap(); + let output: String = final_state.output.unwrap(); + + (ok, output) +} + +fn format_compiler_load_error(e: std::io::Error) -> String { + if matches!(e.kind(), std::io::ErrorKind::NotFound) { + format!( + "\n\n {}\n\n", + [ + "CANNOT BUILD WASM REPL TESTS", + "Please run these tests using repl_test/run_wasm.sh!", + "", + "These tests combine two builds for two different targets,", + "which Cargo doesn't handle very well.", + "It probably requires a second target directory to avoid locks.", + "We'll get to it eventually but it's not done yet.", + ] + .join("\n ") + ) + } else { + format!("{:?}", e) + } +} + +fn dummy_system_time_now() -> f64 { + 0.0 +} + +pub fn expect_success(input: &'static str, expected: &str) { + let (ok, output) = run(input); + assert_eq!(ok, true); + assert_eq!(output, expected); +} + +pub fn expect_failure(input: &'static str, expected: &str) { + let (ok, output) = run(input); + assert_eq!(ok, false); + assert_eq!(output, expected); +} diff --git a/repl_wasm/Cargo.toml b/repl_wasm/Cargo.toml index 03dd9536a2..5e34c1b389 100644 --- a/repl_wasm/Cargo.toml +++ b/repl_wasm/Cargo.toml @@ -11,6 +11,7 @@ roc_builtins = {path = "../compiler/builtins"} [dependencies] bumpalo = {version = "3.8.0", features = ["collections"]} +futures = {version = "0.3.17", optional = true} js-sys = "0.3.56" wasm-bindgen = "0.2.79" wasm-bindgen-futures = "0.4.29" @@ -22,3 +23,6 @@ roc_parse = {path = "../compiler/parse"} roc_repl_eval = {path = "../repl_eval"} roc_target = {path = "../compiler/roc_target"} roc_types = {path = "../compiler/types"} + +[features] +wasmer = ["futures"] diff --git a/repl_wasm/src/interface_js.rs b/repl_wasm/src/interface_js.rs new file mode 100644 index 0000000000..ec29dded39 --- /dev/null +++ b/repl_wasm/src/interface_js.rs @@ -0,0 +1,24 @@ +// wasm_bindgen procedural macro breaks this clippy rule +// https://github.com/rustwasm/wasm-bindgen/issues/2774 +#![allow(clippy::unused_unit)] + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch)] + pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), JsValue>; + + pub fn js_run_app() -> usize; + + pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; +} + +/// Async entrypoint for the browser +/// The browser only has an async API to generate a Wasm module from bytes +/// wasm_bindgen manages the interaction between Rust Futures and JS Promises +#[wasm_bindgen] +pub async fn entrypoint_from_js(src: String) -> Result { + crate::repl::entrypoint_from_js(src).await +} diff --git a/repl_wasm/src/interface_test.rs b/repl_wasm/src/interface_test.rs new file mode 100644 index 0000000000..8fc99f2c87 --- /dev/null +++ b/repl_wasm/src/interface_test.rs @@ -0,0 +1,47 @@ +use futures::executor; + +extern "C" { + fn wasmer_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize); + fn wasmer_run_app() -> usize; + fn wasmer_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; + fn wasmer_copy_input_string(src_buffer_addr: *mut u8); + fn wasmer_copy_output_string(output_ptr: *const u8, output_len: usize); +} + +/// Async wrapper to match the equivalent JS function +pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> { + unsafe { + wasmer_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()); + } + Ok(()) +} + +pub fn js_run_app() -> usize { + unsafe { wasmer_run_app() } +} + +pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize { + unsafe { wasmer_get_result_and_memory(buffer_alloc_addr) } +} + +/// Entrypoint for Wasmer tests +/// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary. +/// (wasmer has a sync API for creating an Instance, whereas browsers don't) +/// - Uses an extra callback to allocate & copy the input string (wasm_bindgen does this for JS) +#[no_mangle] +pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool { + let mut src_buffer = std::vec![0; src_len]; + let src = unsafe { + wasmer_copy_input_string(src_buffer.as_mut_ptr()); + String::from_utf8_unchecked(src_buffer) + }; + let result_async = crate::repl::entrypoint_from_js(src); + let result = executor::block_on(result_async); + let ok = result.is_ok(); + + let output = result.unwrap_or_else(|s| s); + + unsafe { wasmer_copy_output_string(output.as_ptr(), output.len()) } + + ok +} diff --git a/repl_wasm/src/lib.rs b/repl_wasm/src/lib.rs index 48910defba..be90212c28 100644 --- a/repl_wasm/src/lib.rs +++ b/repl_wasm/src/lib.rs @@ -1,258 +1,19 @@ -// wasm_bindgen procedural macro breaks this clippy rule -// https://github.com/rustwasm/wasm-bindgen/issues/2774 -#![allow(clippy::unused_unit)] +mod repl; -use bumpalo::{collections::vec::Vec, Bump}; -use std::mem::size_of; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsValue; +// +// Interface with external JS in the browser +// +#[cfg(not(feature = "wasmer"))] +mod interface_js; +#[cfg(not(feature = "wasmer"))] +pub use interface_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app}; -use roc_collections::all::MutSet; -use roc_gen_wasm::wasm32_result; -use roc_load::file::MonomorphizedModule; -use roc_parse::ast::Expr; -use roc_repl_eval::{ - eval::jit_to_ast, - gen::{compile_to_mono, format_answer, ReplOutput}, - ReplApp, ReplAppMemory, +// +// Interface with test code outside the Wasm module +// +#[cfg(feature = "wasmer")] +mod interface_test; +#[cfg(feature = "wasmer")] +pub use interface_test::{ + entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app, }; -use roc_target::TargetInfo; -use roc_types::pretty_print::{content_to_string, name_all_type_vars}; - -const WRAPPER_NAME: &str = "wrapper"; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(catch)] - async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), JsValue>; - - fn js_run_app() -> usize; - - fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; - - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); -} - -// In-browser debugging -#[allow(unused_macros)] -macro_rules! console_log { - ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) -} - -pub struct WasmReplApp<'a> { - arena: &'a Bump, -} - -/// A copy of the app's memory, made after running the main function -/// The Wasm app ran in a separate address space from the compiler and the eval code. -/// This means we can't simply dereference its pointers as if they were local, because -/// an unrelated value may exist at the same-numbered address in our own address space! -/// Instead we have dereferencing methods that index into the copied bytes. -pub struct WasmMemory<'a> { - copied_bytes: &'a [u8], -} - -macro_rules! deref_number { - ($name: ident, $t: ty) => { - fn $name(&self, address: usize) -> $t { - const N: usize = size_of::<$t>(); - let mut array = [0; N]; - array.copy_from_slice(&self.copied_bytes[address..][..N]); - <$t>::from_le_bytes(array) - } - }; -} - -impl<'a> ReplAppMemory for WasmMemory<'a> { - fn deref_bool(&self, address: usize) -> bool { - self.copied_bytes[address] != 0 - } - - deref_number!(deref_u8, u8); - deref_number!(deref_u16, u16); - deref_number!(deref_u32, u32); - deref_number!(deref_u64, u64); - deref_number!(deref_u128, u128); - deref_number!(deref_usize, usize); - - deref_number!(deref_i8, i8); - deref_number!(deref_i16, i16); - deref_number!(deref_i32, i32); - deref_number!(deref_i64, i64); - deref_number!(deref_i128, i128); - deref_number!(deref_isize, isize); - - deref_number!(deref_f32, f32); - deref_number!(deref_f64, f64); - - fn deref_str(&self, addr: usize) -> &str { - let elems_addr = self.deref_usize(addr); - let len = self.deref_usize(addr + size_of::()); - let bytes = &self.copied_bytes[elems_addr..][..len]; - std::str::from_utf8(bytes).unwrap() - } -} - -impl<'a> ReplApp<'a> for WasmReplApp<'a> { - type Memory = WasmMemory<'a>; - - /// Run user code that returns a type with a `Builtin` layout - /// Size of the return value is statically determined from its Rust type - /// The `transform` callback takes the app's memory and the returned value - /// _main_fn_name is always the same and we don't use it here - fn call_function(&self, _main_fn_name: &str, transform: F) -> Expr<'a> - where - F: Fn(&'a Self::Memory, Return) -> Expr<'a>, - Self::Memory: 'a, - { - let app_final_memory_size: usize = js_run_app(); - - // Allocate a buffer to copy the app memory into - // Aligning it to 64 bits will preserve the original alignment of all Wasm numbers - let copy_buffer_aligned: &mut [u64] = self - .arena - .alloc_slice_fill_default((app_final_memory_size / size_of::()) + 1); - let copied_bytes: &mut [u8] = unsafe { std::mem::transmute(copy_buffer_aligned) }; - - let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); - - let result: Return = unsafe { - let ptr: *const Return = std::mem::transmute(&copied_bytes[app_result_addr]); - ptr.read() - }; - let mem = self.arena.alloc(WasmMemory { copied_bytes }); - - transform(mem, result) - } - - /// Run user code that returns a struct or union, whose size is provided as an argument - /// The `transform` callback takes the app's memory and the address of the returned value - /// _main_fn_name and _ret_bytes are only used for the CLI REPL. For Wasm they are compiled-in - /// to the test_wrapper function of the app itself - fn call_function_dynamic_size( - &self, - _main_fn_name: &str, - _ret_bytes: usize, - transform: F, - ) -> T - where - F: Fn(&'a Self::Memory, usize) -> T, - Self::Memory: 'a, - { - let app_final_memory_size: usize = js_run_app(); - - // Allocate a buffer to copy the app memory into - // Aligning it to 64 bits will preserve the original alignment of all Wasm numbers - let copy_buffer_aligned: &mut [u64] = self - .arena - .alloc_slice_fill_default((app_final_memory_size / size_of::()) + 1); - let copied_bytes: &mut [u8] = unsafe { std::mem::transmute(copy_buffer_aligned) }; - - let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); - let mem = self.arena.alloc(WasmMemory { copied_bytes }); - - transform(mem, app_result_addr) - } -} - -#[wasm_bindgen] -pub async fn entrypoint_from_js(src: String) -> Result { - let arena = &Bump::new(); - let pre_linked_binary: &'static [u8] = include_bytes!("../data/pre_linked_binary.o"); - - // Compile the app - let target_info = TargetInfo::default_wasm32(); - let mono = match compile_to_mono(arena, &src, target_info) { - Ok(m) => m, - Err(messages) => return Err(messages.join("\n\n")), - }; - - let MonomorphizedModule { - module_id, - procedures, - mut interns, - mut subs, - exposed_to_host, - .. - } = mono; - - debug_assert_eq!(exposed_to_host.values.len(), 1); - let (main_fn_symbol, main_fn_var) = exposed_to_host.values.iter().next().unwrap(); - let main_fn_symbol = *main_fn_symbol; - let main_fn_var = *main_fn_var; - - // pretty-print the expr type string for later. - name_all_type_vars(main_fn_var, &mut subs); - let content = subs.get_content_without_compacting(main_fn_var); - let expr_type_str = content_to_string(content, &subs, module_id, &interns); - - let (_, main_fn_layout) = match procedures.keys().find(|(s, _)| *s == main_fn_symbol) { - Some(layout) => *layout, - None => return Ok(format!(" : {}", expr_type_str)), - }; - - let app_module_bytes = { - let env = roc_gen_wasm::Env { - arena, - module_id, - exposed_to_host: exposed_to_host - .values - .keys() - .copied() - .collect::>(), - }; - - let (mut module, called_preload_fns, main_fn_index) = { - roc_gen_wasm::build_module_without_wrapper( - &env, - &mut interns, // NOTE: must drop this mutable ref before jit_to_ast - pre_linked_binary, - procedures, - ) - }; - - wasm32_result::insert_wrapper_for_layout( - arena, - &mut module, - WRAPPER_NAME, - main_fn_index, - &main_fn_layout.result, - ); - - module.remove_dead_preloads(env.arena, called_preload_fns); - - let mut buffer = Vec::with_capacity_in(module.size(), arena); - module.serialize(&mut buffer); - - buffer - }; - - // Send the compiled binary out to JS and create an executable instance from it - js_create_app(&app_module_bytes) - .await - .map_err(|js| format!("{:?}", js))?; - - let app = WasmReplApp { arena }; - - // Run the app and transform the result value to an AST `Expr` - // Restore type constructor names, and other user-facing info that was erased during compilation. - let res_answer = jit_to_ast( - arena, - &app, - "", // main_fn_name is ignored (only passed to WasmReplApp methods) - main_fn_layout, - content, - &interns, - module_id, - &subs, - target_info, - ); - - // Transform the Expr to a string - // `Result::Err` becomes a JS exception that will be caught and displayed - match format_answer(arena, res_answer, expr_type_str) { - ReplOutput::NoProblems { expr, expr_type } => Ok(format!("\n{}: {}", expr, expr_type)), - ReplOutput::Problems(lines) => Err(format!("\n{}\n", lines.join("\n\n"))), - } -} diff --git a/repl_wasm/src/repl.rs b/repl_wasm/src/repl.rs new file mode 100644 index 0000000000..5c3686b664 --- /dev/null +++ b/repl_wasm/src/repl.rs @@ -0,0 +1,243 @@ +use bumpalo::{collections::vec::Vec, Bump}; +use std::mem::size_of; + +use roc_collections::all::MutSet; +use roc_gen_wasm::wasm32_result; +use roc_load::file::MonomorphizedModule; +use roc_parse::ast::Expr; +use roc_repl_eval::{ + eval::jit_to_ast, + gen::{compile_to_mono, format_answer, ReplOutput}, + ReplApp, ReplAppMemory, +}; +use roc_target::TargetInfo; +use roc_types::pretty_print::{content_to_string, name_all_type_vars}; + +use crate::{js_create_app, js_get_result_and_memory, js_run_app}; + +const WRAPPER_NAME: &str = "wrapper"; + +pub struct WasmReplApp<'a> { + arena: &'a Bump, +} + +/// A copy of the app's memory, made after running the main function +/// The Wasm app ran in a separate address space from the compiler and the eval code. +/// This means we can't simply dereference its pointers as if they were local, because +/// an unrelated value may exist at the same-numbered address in our own address space! +/// Instead we have dereferencing methods that index into the copied bytes. +pub struct WasmMemory<'a> { + copied_bytes: &'a [u8], +} + +macro_rules! deref_number { + ($name: ident, $t: ty) => { + fn $name(&self, address: usize) -> $t { + const N: usize = size_of::<$t>(); + let mut array = [0; N]; + array.copy_from_slice(&self.copied_bytes[address..][..N]); + <$t>::from_le_bytes(array) + } + }; +} + +impl<'a> ReplAppMemory for WasmMemory<'a> { + fn deref_bool(&self, address: usize) -> bool { + self.copied_bytes[address] != 0 + } + + deref_number!(deref_u8, u8); + deref_number!(deref_u16, u16); + deref_number!(deref_u32, u32); + deref_number!(deref_u64, u64); + deref_number!(deref_u128, u128); + deref_number!(deref_usize, usize); + + deref_number!(deref_i8, i8); + deref_number!(deref_i16, i16); + deref_number!(deref_i32, i32); + deref_number!(deref_i64, i64); + deref_number!(deref_i128, i128); + deref_number!(deref_isize, isize); + + deref_number!(deref_f32, f32); + deref_number!(deref_f64, f64); + + fn deref_str(&self, addr: usize) -> &str { + let elems_addr = self.deref_usize(addr); + let len = self.deref_usize(addr + size_of::()); + let bytes = &self.copied_bytes[elems_addr..][..len]; + std::str::from_utf8(bytes).unwrap() + } +} + +impl<'a> WasmReplApp<'a> { + /// Allocate a buffer to copy the app memory into + /// Buffer is aligned to 64 bits to preserve the original alignment of all Wasm numbers + fn allocate_buffer(&self, size: usize) -> &'a mut [u8] { + let size64 = (size / size_of::()) + 1; + let buffer64: &mut [u64] = self.arena.alloc_slice_fill_default(size64); + + // Note: Need `from_raw_parts_mut` as well as `transmute` to ensure slice has correct length! + let buffer: &mut [u8] = unsafe { + let ptr8: *mut u8 = std::mem::transmute(buffer64.as_mut_ptr()); + std::slice::from_raw_parts_mut(ptr8, size) + }; + + buffer + } +} + +impl<'a> ReplApp<'a> for WasmReplApp<'a> { + type Memory = WasmMemory<'a>; + + /// Run user code that returns a type with a `Builtin` layout + /// Size of the return value is statically determined from its Rust type + /// The `transform` callback takes the app's memory and the returned value + /// _main_fn_name is always the same and we don't use it here + fn call_function(&self, _main_fn_name: &str, transform: F) -> Expr<'a> + where + F: Fn(&'a Self::Memory, Return) -> Expr<'a>, + Self::Memory: 'a, + { + let app_final_memory_size: usize = js_run_app(); + + let copied_bytes: &mut [u8] = self.allocate_buffer(app_final_memory_size); + + let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); + + let result_bytes = &copied_bytes[app_result_addr..]; + let result: Return = unsafe { + let ptr: *const Return = std::mem::transmute(result_bytes.as_ptr()); + ptr.read() + }; + + let mem = self.arena.alloc(WasmMemory { copied_bytes }); + + transform(mem, result) + } + + /// Run user code that returns a struct or union, whose size is provided as an argument + /// The `transform` callback takes the app's memory and the address of the returned value + /// _main_fn_name and _ret_bytes are only used for the CLI REPL. For Wasm they are compiled-in + /// to the test_wrapper function of the app itself + fn call_function_dynamic_size( + &self, + _main_fn_name: &str, + _ret_bytes: usize, + transform: F, + ) -> T + where + F: Fn(&'a Self::Memory, usize) -> T, + Self::Memory: 'a, + { + let app_final_memory_size: usize = js_run_app(); + + let copied_bytes: &mut [u8] = self.allocate_buffer(app_final_memory_size); + + let app_result_addr = js_get_result_and_memory(copied_bytes.as_mut_ptr()); + let mem = self.arena.alloc(WasmMemory { copied_bytes }); + + transform(mem, app_result_addr) + } +} + +pub async fn entrypoint_from_js(src: String) -> Result { + let arena = &Bump::new(); + let pre_linked_binary: &'static [u8] = include_bytes!("../data/pre_linked_binary.o"); + + // Compile the app + let target_info = TargetInfo::default_wasm32(); + let mono = match compile_to_mono(arena, &src, target_info) { + Ok(m) => m, + Err(messages) => return Err(messages.join("\n\n")), + }; + + let MonomorphizedModule { + module_id, + procedures, + mut interns, + mut subs, + exposed_to_host, + .. + } = mono; + + debug_assert_eq!(exposed_to_host.values.len(), 1); + let (main_fn_symbol, main_fn_var) = exposed_to_host.values.iter().next().unwrap(); + let main_fn_symbol = *main_fn_symbol; + let main_fn_var = *main_fn_var; + + // pretty-print the expr type string for later. + name_all_type_vars(main_fn_var, &mut subs); + let content = subs.get_content_without_compacting(main_fn_var); + let expr_type_str = content_to_string(content, &subs, module_id, &interns); + + let (_, main_fn_layout) = match procedures.keys().find(|(s, _)| *s == main_fn_symbol) { + Some(layout) => *layout, + None => return Ok(format!(" : {}", expr_type_str)), + }; + + let app_module_bytes = { + let env = roc_gen_wasm::Env { + arena, + module_id, + exposed_to_host: exposed_to_host + .values + .keys() + .copied() + .collect::>(), + }; + + let (mut module, called_preload_fns, main_fn_index) = { + roc_gen_wasm::build_module_without_wrapper( + &env, + &mut interns, // NOTE: must drop this mutable ref before jit_to_ast + pre_linked_binary, + procedures, + ) + }; + + wasm32_result::insert_wrapper_for_layout( + arena, + &mut module, + WRAPPER_NAME, + main_fn_index, + &main_fn_layout.result, + ); + + module.remove_dead_preloads(env.arena, called_preload_fns); + + let mut buffer = Vec::with_capacity_in(module.size(), arena); + module.serialize(&mut buffer); + + buffer + }; + + // Send the compiled binary out to JS and create an executable instance from it + js_create_app(&app_module_bytes) + .await + .map_err(|js| format!("{:?}", js))?; + + let app = WasmReplApp { arena }; + + // Run the app and transform the result value to an AST `Expr` + // Restore type constructor names, and other user-facing info that was erased during compilation. + let res_answer = jit_to_ast( + arena, + &app, + "", // main_fn_name is ignored (only passed to WasmReplApp methods) + main_fn_layout, + content, + &interns, + module_id, + &subs, + target_info, + ); + + // Transform the Expr to a string + // `Result::Err` becomes a JS exception that will be caught and displayed + match format_answer(arena, res_answer, expr_type_str) { + ReplOutput::NoProblems { expr, expr_type } => Ok(format!("{} : {}", expr, expr_type)), + ReplOutput::Problems(lines) => Err(format!("\n{}\n", lines.join("\n\n"))), + } +} diff --git a/repl_www/build.sh b/repl_www/build.sh index 549dbfaf1c..bf1d944864 100755 --- a/repl_www/build.sh +++ b/repl_www/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -eux +set -ex if [[ ! -d repl_www ]] then @@ -17,12 +17,15 @@ WWW_DIR="repl_www/build" mkdir -p $WWW_DIR cp repl_www/public/* $WWW_DIR -# Pass all script arguments through to wasm-pack # For debugging, pass the --profiling option, which enables optimizations + debug info # (We need optimizations to get rid of dead code that otherwise causes compile errors!) -cargo build --target wasm32-unknown-unknown -p roc_repl_wasm --release -wasm-bindgen --target web --keep-debug target/wasm32-unknown-unknown/release/roc_repl_wasm.wasm --out-dir repl_wasm/pkg/ -# wasm-pack build --target web "$@" repl_wasm +if [ -n "$REPL_DEBUG" ] +then + cargo build --target wasm32-unknown-unknown -p roc_repl_wasm --release + wasm-bindgen --target web --keep-debug target/wasm32-unknown-unknown/release/roc_repl_wasm.wasm --out-dir repl_wasm/pkg/ +else + wasm-pack build --target web repl_wasm +fi cp repl_wasm/pkg/*.wasm $WWW_DIR diff --git a/repl_www/public/index.html b/repl_www/public/index.html index 20e4ed3ba2..e08ec457a1 100644 --- a/repl_www/public/index.html +++ b/repl_www/public/index.html @@ -70,7 +70,7 @@ margin-bottom: 16px; } - Mock REPL + Roc REPL
diff --git a/repl_www/public/repl.js b/repl_www/public/repl.js index ecd07d8b5b..350ac9a066 100644 --- a/repl_www/public/repl.js +++ b/repl_www/public/repl.js @@ -3,6 +3,7 @@ window.js_create_app = js_create_app; window.js_run_app = js_run_app; window.js_get_result_and_memory = js_get_result_and_memory; import * as roc_repl_wasm from "./roc_repl_wasm.js"; +import { getMockWasiImports } from "./wasi.js"; // ---------------------------------------------------------------------------- // REPL state @@ -74,15 +75,21 @@ async function processInputQueue() { // Create an executable Wasm instance from an array of bytes // (Browser validates the module and does the final compilation to the host's machine code.) async function js_create_app(wasm_module_bytes) { - const { instance } = await WebAssembly.instantiate(wasm_module_bytes); + const wasiLinkObject = {}; // gives the WASI functions a reference to the app so they can write to its memory + const importObj = getMockWasiImports(wasiLinkObject); + const { instance } = await WebAssembly.instantiate( + wasm_module_bytes, + importObj + ); + wasiLinkObject.instance = instance; repl.app = instance; } // Call the main function of the app, via the test wrapper // Cache the result and return the size of the app's memory function js_run_app() { - const { run, memory } = repl.app.exports; - const addr = run(); + const { wrapper, memory } = repl.app.exports; + const addr = wrapper(); const { buffer } = memory; repl.result = { addr, buffer }; diff --git a/repl_www/public/wasi.js b/repl_www/public/wasi.js new file mode 100644 index 0000000000..f636d055c7 --- /dev/null +++ b/repl_www/public/wasi.js @@ -0,0 +1,192 @@ +/** + * Browser implementation of the WebAssembly System Interface (WASI) + * The REPL generates an "app" from the user's Roc code and it can import from WASI + * + * We only implement writes to stdout/stderr as console.log/console.err, and proc_exit as `throw` + * The rest of the interface just consists of dummy functions with the right number of arguments + * + * The wasiLinkObject provides a reference to the app so we can write to its memory + */ + +export function getMockWasiImports(wasiLinkObject) { + const decoder = new TextDecoder(); + + // If the app ever resizes its memory, there will be a new buffer instance + // so we get a fresh reference every time, just in case + function getMemory8() { + return new Uint8Array(wasiLinkObject.instance.exports.memory.buffer); + } + + function getMemory32() { + return new Uint32Array(wasiLinkObject.instance.exports.memory.buffer); + } + + // 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; + const memory8 = getMemory8(); + memory8[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE; + 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 memory32 = getMemory32(); + + for (let i = 0; i < iovs_len; i++) { + const index32 = iovs_ptr >> 2; + const base = memory32[index32]; + const len = 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 = getMemory8().slice(base, base + len); + const chunk = decoder.decode(buf); + string_buffer += chunk; + } + memory32[nwritten_mut_ptr >> 2] = nwritten; + if (string_buffer) { + console.log(string_buffer); + } + return 0; + } + + // proc_exit : (i32) -> nil + function proc_exit(exit_code) { + if (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, + }, + }; +}