Merge pull request #2517 from rtfeldman/repl-www

Web REPL initial version
This commit is contained in:
Brian Carroll 2022-02-21 13:58:23 +00:00 committed by GitHub
commit e8571de1ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 915 additions and 391 deletions

140
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

3
cli_utils/Cargo.lock generated
View file

@ -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]]

View file

@ -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"

View file

@ -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 = <T as crate::helpers::llvm::FromWasmerMemory>::decode(
memory,

View file

@ -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

View file

@ -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 = []

6
repl_test/run_wasm.sh Executable file
View file

@ -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

View file

@ -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", "<function> : a -> a");
}
#[cfg(not(feature = "wasm"))]
#[test]
fn sum_lambda() {
expect_success("\\x, y -> x + y", "<function> : Num a, Num a -> Num a");
}
#[cfg(not(feature = "wasm"))]
#[test]
fn stdlib_function() {
expect_success("Num.abs", "<function> : 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(

261
repl_test/src/wasm.rs Normal file
View file

@ -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<Option<Instance>> = RefCell::new(None)
}
thread_local! {
static REPL_STATE: RefCell<Option<ReplState>> = RefCell::new(None)
}
struct ReplState {
src: &'static str,
app: Option<Instance>,
result_addr: Option<u32>,
output: Option<String>,
}
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);
}

View file

@ -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"]

View file

@ -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<String, String> {
crate::repl::entrypoint_from_js(src).await
}

View file

@ -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
}

View file

@ -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::<usize>());
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<Return, F>(&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::<u64>()) + 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<T, F>(
&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::<u64>()) + 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<String, String> {
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!("<function> : {}", 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::<MutSet<_>>(),
};
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"))),
}
}

243
repl_wasm/src/repl.rs Normal file
View file

@ -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::<usize>());
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::<u64>()) + 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<Return, F>(&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<T, F>(
&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<String, String> {
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!("<function> : {}", 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::<MutSet<_>>(),
};
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"))),
}
}

View file

@ -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!)
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/
# wasm-pack build --target web "$@" repl_wasm
else
wasm-pack build --target web repl_wasm
fi
cp repl_wasm/pkg/*.wasm $WWW_DIR

View file

@ -70,7 +70,7 @@
margin-bottom: 16px;
}
</style>
<title>Mock REPL</title>
<title>Roc REPL</title>
</head>
<body>
<div class="body-wrapper">

View file

@ -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 };

192
repl_www/public/wasi.js Normal file
View file

@ -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,
},
};
}