mirror of
https://github.com/roc-lang/roc.git
synced 2025-11-17 02:53:30 +00:00
Merge pull request #4767 from roc-lang/wasm_interp_repl_test
Replace Wasmer with roc_wasm_interp
This commit is contained in:
commit
a18197347b
24 changed files with 531 additions and 1411 deletions
3
.github/workflows/ubuntu_x86_64.yml
vendored
3
.github/workflows/ubuntu_x86_64.yml
vendored
|
|
@ -31,7 +31,7 @@ jobs:
|
||||||
run: cargo run --locked --release format --check crates/compiler/builtins/roc
|
run: cargo run --locked --release format --check crates/compiler/builtins/roc
|
||||||
|
|
||||||
- name: zig wasm tests
|
- name: zig wasm tests
|
||||||
run: cargo build --release -p roc_wasm_interp && cd crates/compiler/builtins/bitcode && ./run-wasm-tests.sh
|
run: cd crates/compiler/builtins/bitcode && ./run-wasm-tests.sh
|
||||||
|
|
||||||
- name: regular rust tests
|
- name: regular rust tests
|
||||||
run: cargo test --locked --release && sccache --show-stats
|
run: cargo test --locked --release && sccache --show-stats
|
||||||
|
|
@ -54,7 +54,6 @@ jobs:
|
||||||
- name: run `roc test` on Dict builtins
|
- name: run `roc test` on Dict builtins
|
||||||
run: cargo run --locked --release -- test crates/compiler/builtins/roc/Dict.roc && sccache --show-stats
|
run: cargo run --locked --release -- test crates/compiler/builtins/roc/Dict.roc && sccache --show-stats
|
||||||
|
|
||||||
#TODO pass --locked into the script here as well, this avoids rebuilding dependencies unnecessarily
|
|
||||||
- name: wasm repl test
|
- name: wasm repl test
|
||||||
run: crates/repl_test/test_wasm.sh && sccache --show-stats
|
run: crates/repl_test/test_wasm.sh && sccache --show-stats
|
||||||
|
|
||||||
|
|
|
||||||
895
Cargo.lock
generated
895
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -22,7 +22,7 @@ i386-cli-run = ["target-x86"]
|
||||||
|
|
||||||
editor = ["roc_editor"]
|
editor = ["roc_editor"]
|
||||||
|
|
||||||
run-wasm32 = ["wasmer", "wasmer-wasi"]
|
run-wasm32 = ["roc_wasm_interp"]
|
||||||
|
|
||||||
# Compiling for a different target than the current machine can cause linker errors.
|
# Compiling for a different target than the current machine can cause linker errors.
|
||||||
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
|
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
|
||||||
|
|
@ -65,11 +65,10 @@ roc_repl_cli = { path = "../repl_cli", optional = true }
|
||||||
roc_tracing = { path = "../tracing" }
|
roc_tracing = { path = "../tracing" }
|
||||||
roc_intern = { path = "../compiler/intern" }
|
roc_intern = { path = "../compiler/intern" }
|
||||||
roc_gen_llvm = {path = "../compiler/gen_llvm"}
|
roc_gen_llvm = {path = "../compiler/gen_llvm"}
|
||||||
|
roc_wasm_interp = { path = "../wasm_interp", optional = true }
|
||||||
|
|
||||||
ven_pretty = { path = "../vendor/pretty" }
|
ven_pretty = { path = "../vendor/pretty" }
|
||||||
|
|
||||||
wasmer-wasi = { version = "2.2.1", optional = true }
|
|
||||||
|
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
const_format.workspace = true
|
const_format.workspace = true
|
||||||
mimalloc.workspace = true
|
mimalloc.workspace = true
|
||||||
|
|
@ -88,15 +87,8 @@ inkwell.workspace = true
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
roc_repl_expect = { path = "../repl_expect" }
|
roc_repl_expect = { path = "../repl_expect" }
|
||||||
|
|
||||||
# Wasmer singlepass compiler only works on x86_64.
|
|
||||||
[target.'cfg(target_arch = "x86_64")'.dependencies]
|
|
||||||
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["singlepass", "universal"] }
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "x86_64"))'.dependencies]
|
|
||||||
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["cranelift", "universal"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasmer-wasi = "2.2.1"
|
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
roc_test_utils = { path = "../test_utils" }
|
roc_test_utils = { path = "../test_utils" }
|
||||||
roc_utils = { path = "../utils" }
|
roc_utils = { path = "../utils" }
|
||||||
|
|
@ -107,13 +99,6 @@ cli_utils = { path = "../cli_utils" }
|
||||||
once_cell = "1.15.0"
|
once_cell = "1.15.0"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
|
||||||
# Wasmer singlepass compiler only works on x86_64.
|
|
||||||
[target.'cfg(target_arch = "x86_64")'.dev-dependencies]
|
|
||||||
wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] }
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies]
|
|
||||||
wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] }
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "time_bench"
|
name = "time_bench"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
|
||||||
|
|
@ -853,7 +853,7 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
|
||||||
{
|
{
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
run_with_wasmer(
|
run_wasm(
|
||||||
generated_filename,
|
generated_filename,
|
||||||
args.into_iter().map(|os_str| os_str.as_bytes()),
|
args.into_iter().map(|os_str| os_str.as_bytes()),
|
||||||
);
|
);
|
||||||
|
|
@ -861,11 +861,11 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
|
||||||
|
|
||||||
#[cfg(not(target_family = "unix"))]
|
#[cfg(not(target_family = "unix"))]
|
||||||
{
|
{
|
||||||
run_with_wasmer(
|
run_wasm(
|
||||||
generated_filename,
|
generated_filename,
|
||||||
args.into_iter().map(|os_str| {
|
args.into_iter().map(|os_str| {
|
||||||
os_str.to_str().expect(
|
os_str.to_str().expect(
|
||||||
"Roc does not currently support passing non-UTF8 arguments to Wasmer.",
|
"Roc does not currently support passing non-UTF8 arguments to Wasm.",
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -1239,38 +1239,33 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "run-wasm32")]
|
#[cfg(feature = "run-wasm32")]
|
||||||
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
|
fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
|
||||||
use wasmer::{Instance, Module, Store};
|
use bumpalo::collections::Vec;
|
||||||
|
use roc_wasm_interp::{DefaultImportDispatcher, Instance};
|
||||||
|
|
||||||
let store = Store::default();
|
let bytes = std::fs::read(wasm_path).unwrap();
|
||||||
let module = Module::from_file(&store, &wasm_path).unwrap();
|
let arena = Bump::new();
|
||||||
|
|
||||||
// First, we create the `WasiEnv`
|
let mut argv = Vec::<&[u8]>::new_in(&arena);
|
||||||
use wasmer_wasi::WasiState;
|
for arg in args {
|
||||||
let mut wasi_env = WasiState::new("hello").args(args).finalize().unwrap();
|
let mut arg_copy = Vec::<u8>::new_in(&arena);
|
||||||
|
arg_copy.extend_from_slice(arg.as_ref());
|
||||||
// Then, we get the import object related to our WASI
|
argv.push(arg_copy.into_bump_slice());
|
||||||
// and attach it to the Wasm instance.
|
|
||||||
let import_object = wasi_env.import_object(&module).unwrap();
|
|
||||||
|
|
||||||
let instance = Instance::new(&module, &import_object).unwrap();
|
|
||||||
|
|
||||||
let start = instance.exports.get_function("_start").unwrap();
|
|
||||||
|
|
||||||
use wasmer_wasi::WasiError;
|
|
||||||
match start.call(&[]) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => match e.downcast::<WasiError>() {
|
|
||||||
Ok(WasiError::Exit(0)) => {
|
|
||||||
// we run the `_start` function, so exit(0) is expected
|
|
||||||
}
|
|
||||||
other => panic!("Wasmer error: {:?}", other),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
let import_dispatcher = DefaultImportDispatcher::new(&argv);
|
||||||
|
|
||||||
|
let mut instance = Instance::from_bytes(&arena, &bytes, import_dispatcher, false).unwrap();
|
||||||
|
|
||||||
|
instance
|
||||||
|
.call_export("_start", [])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.expect_i32()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "run-wasm32"))]
|
#[cfg(not(feature = "run-wasm32"))]
|
||||||
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
|
fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
|
||||||
println!("Running wasm files is not supported on this target.");
|
println!("Running wasm files is not supported on this target.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1014,7 +1014,7 @@ mod cli_run {
|
||||||
let mut path = file.with_file_name(executable_filename);
|
let mut path = file.with_file_name(executable_filename);
|
||||||
path.set_extension("wasm");
|
path.set_extension("wasm");
|
||||||
|
|
||||||
let stdout = crate::run_with_wasmer(&path, stdin);
|
let stdout = crate::run_wasm(&path, stdin);
|
||||||
|
|
||||||
if !stdout.ends_with(expected_ending) {
|
if !stdout.ends_with(expected_ending) {
|
||||||
panic!(
|
panic!(
|
||||||
|
|
@ -1363,75 +1363,49 @@ mod cli_run {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[cfg(feature = "wasm32-cli-run")]
|
||||||
fn run_with_wasmer(wasm_path: &std::path::Path, stdin: &[&str]) -> String {
|
fn run_wasm(wasm_path: &std::path::Path, stdin: &[&str]) -> String {
|
||||||
use std::io::Write;
|
use bumpalo::Bump;
|
||||||
use wasmer::{Instance, Module, Store};
|
use roc_wasm_interp::{DefaultImportDispatcher, Instance, Value, WasiFile};
|
||||||
|
|
||||||
// std::process::Command::new("cp")
|
let wasm_bytes = std::fs::read(wasm_path).unwrap();
|
||||||
// .args(&[
|
let arena = Bump::new();
|
||||||
// wasm_path.to_str().unwrap(),
|
|
||||||
// "/home/folkertdev/roc/wasm/nqueens.wasm",
|
|
||||||
// ])
|
|
||||||
// .output()
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
let store = Store::default();
|
let mut instance = {
|
||||||
let module = Module::from_file(&store, wasm_path).unwrap();
|
let mut fake_stdin = vec![];
|
||||||
|
let fake_stdout = vec![];
|
||||||
|
let fake_stderr = vec![];
|
||||||
|
for s in stdin {
|
||||||
|
fake_stdin.extend_from_slice(s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
let mut fake_stdin = wasmer_wasi::Pipe::new();
|
let mut dispatcher = DefaultImportDispatcher::default();
|
||||||
let fake_stdout = wasmer_wasi::Pipe::new();
|
dispatcher.wasi.files = vec![
|
||||||
let fake_stderr = wasmer_wasi::Pipe::new();
|
WasiFile::ReadOnly(fake_stdin),
|
||||||
|
WasiFile::WriteOnly(fake_stdout),
|
||||||
|
WasiFile::WriteOnly(fake_stderr),
|
||||||
|
];
|
||||||
|
|
||||||
for line in stdin {
|
Instance::from_bytes(&arena, &wasm_bytes, dispatcher, false).unwrap()
|
||||||
write!(fake_stdin, "{}", line).unwrap();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// First, we create the `WasiEnv`
|
let result = instance.call_export("_start", []);
|
||||||
use wasmer_wasi::WasiState;
|
|
||||||
let mut wasi_env = WasiState::new("hello")
|
|
||||||
.stdin(Box::new(fake_stdin))
|
|
||||||
.stdout(Box::new(fake_stdout))
|
|
||||||
.stderr(Box::new(fake_stderr))
|
|
||||||
.finalize()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Then, we get the import object related to our WASI
|
match result {
|
||||||
// and attach it to the Wasm instance.
|
Ok(Some(Value::I32(0))) => match &instance.import_dispatcher.wasi.files[1] {
|
||||||
let import_object = wasi_env
|
WasiFile::WriteOnly(fake_stdout) => String::from_utf8(fake_stdout.clone())
|
||||||
.import_object(&module)
|
.unwrap_or_else(|_| "Wasm test printed invalid UTF-8".into()),
|
||||||
.unwrap_or_else(|_| wasmer::imports!());
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
let instance = Instance::new(&module, &import_object).unwrap();
|
Ok(Some(Value::I32(exit_code))) => {
|
||||||
|
format!("WASI app exit code {}", exit_code)
|
||||||
let start = instance.exports.get_function("_start").unwrap();
|
}
|
||||||
|
Ok(Some(val)) => {
|
||||||
match start.call(&[]) {
|
format!("WASI _start returned an unexpected number type {:?}", val)
|
||||||
Ok(_) => read_wasi_stdout(wasi_env),
|
}
|
||||||
|
Ok(None) => "WASI _start returned no value".into(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
use wasmer_wasi::WasiError;
|
format!("WASI error {}", e)
|
||||||
match e.downcast::<WasiError>() {
|
|
||||||
Ok(WasiError::Exit(0)) => {
|
|
||||||
// we run the `_start` function, so exit(0) is expected
|
|
||||||
read_wasi_stdout(wasi_env)
|
|
||||||
}
|
|
||||||
other => format!("Something went wrong running a wasm test: {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn read_wasi_stdout(wasi_env: wasmer_wasi::WasiEnv) -> String {
|
|
||||||
let mut state = wasi_env.state.lock().unwrap();
|
|
||||||
|
|
||||||
match state.fs.stdout_mut() {
|
|
||||||
Ok(Some(stdout)) => {
|
|
||||||
let mut buf = String::new();
|
|
||||||
stdout.read_to_string(&mut buf).unwrap();
|
|
||||||
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
|
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
# Test failures will always point at the _start function
|
# For non-native binaries, Zig test needs a "test command" it can use
|
||||||
# Make sure to look at the rest of the stack trace!
|
cargo build --locked --release -p roc_wasm_interp
|
||||||
|
|
||||||
# Zig will try to run the test binary it produced, but since your OS doesn't know how to
|
|
||||||
# run Wasm binaries natively, we need to provide a Wasm interpreter as a "test command".
|
|
||||||
zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ../../../../target/release/roc_wasm_interp --test-cmd-bin
|
zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ../../../../target/release/roc_wasm_interp --test-cmd-bin
|
||||||
|
|
|
||||||
|
|
@ -237,11 +237,11 @@ where
|
||||||
T: FromWasm32Memory + Wasm32Result,
|
T: FromWasm32Memory + Wasm32Result,
|
||||||
{
|
{
|
||||||
let dispatcher = TestDispatcher {
|
let dispatcher = TestDispatcher {
|
||||||
wasi: wasi::WasiDispatcher { args: &[] },
|
wasi: wasi::WasiDispatcher::default(),
|
||||||
};
|
};
|
||||||
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
|
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
|
||||||
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
||||||
let opt_value = inst.call_export(module, test_wrapper_name, [])?;
|
let opt_value = inst.call_export(test_wrapper_name, [])?;
|
||||||
let addr_value = opt_value.ok_or("No return address from Wasm test")?;
|
let addr_value = opt_value.ok_or("No return address from Wasm test")?;
|
||||||
let addr = addr_value.expect_i32().map_err(|e| format!("{:?}", e))?;
|
let addr = addr_value.expect_i32().map_err(|e| format!("{:?}", e))?;
|
||||||
let output = <T as FromWasm32Memory>::decode(&inst.memory, addr as u32);
|
let output = <T as FromWasm32Memory>::decode(&inst.memory, addr as u32);
|
||||||
|
|
@ -266,25 +266,21 @@ where
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
|
||||||
let dispatcher = TestDispatcher {
|
let dispatcher = TestDispatcher {
|
||||||
wasi: wasi::WasiDispatcher { args: &[] },
|
wasi: wasi::WasiDispatcher::default(),
|
||||||
};
|
};
|
||||||
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
|
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
|
||||||
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
||||||
|
|
||||||
// Allocate a vector in the test host that refcounts will be copied into
|
// Allocate a vector in the test host that refcounts will be copied into
|
||||||
let mut refcount_vector_addr: i32 = inst
|
let mut refcount_vector_addr: i32 = inst
|
||||||
.call_export(
|
.call_export(INIT_REFCOUNT_NAME, [Value::I32(num_refcounts as i32)])?
|
||||||
&module,
|
|
||||||
INIT_REFCOUNT_NAME,
|
|
||||||
[Value::I32(num_refcounts as i32)],
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| format!("No return address from {}", INIT_REFCOUNT_NAME))?
|
.ok_or_else(|| format!("No return address from {}", INIT_REFCOUNT_NAME))?
|
||||||
.expect_i32()
|
.expect_i32()
|
||||||
.map_err(|type_err| format!("{:?}", type_err))?;
|
.map_err(|type_err| format!("{:?}", type_err))?;
|
||||||
|
|
||||||
// Run the test, ignoring the result
|
// Run the test, ignoring the result
|
||||||
let _result_addr: i32 = inst
|
let _result_addr: i32 = inst
|
||||||
.call_export(&module, TEST_WRAPPER_NAME, [])?
|
.call_export(TEST_WRAPPER_NAME, [])?
|
||||||
.ok_or_else(|| format!("No return address from {}", TEST_WRAPPER_NAME))?
|
.ok_or_else(|| format!("No return address from {}", TEST_WRAPPER_NAME))?
|
||||||
.expect_i32()
|
.expect_i32()
|
||||||
.map_err(|type_err| format!("{:?}", type_err))?;
|
.map_err(|type_err| format!("{:?}", type_err))?;
|
||||||
|
|
|
||||||
|
|
@ -225,22 +225,22 @@ fn execute_wasm_module<'a>(arena: &'a Bump, orig_module: WasmModule<'a>) -> Resu
|
||||||
};
|
};
|
||||||
|
|
||||||
let dispatcher = TestDispatcher {
|
let dispatcher = TestDispatcher {
|
||||||
wasi: wasi::WasiDispatcher { args: &[] },
|
wasi: wasi::WasiDispatcher::default(),
|
||||||
};
|
};
|
||||||
let is_debug_mode = true;
|
let is_debug_mode = false;
|
||||||
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
|
||||||
|
|
||||||
// In Zig, main can only return u8 or void, but our result is too wide for that.
|
// In Zig, main can only return u8 or void, but our result is too wide for that.
|
||||||
// But I want to use main so that I can test that _start is created for it!
|
// But I want to use main so that I can test that _start is created for it!
|
||||||
// So return void from main, and call another function to get the result.
|
// So return void from main, and call another function to get the result.
|
||||||
inst.call_export(&module, "_start", [])?;
|
inst.call_export("_start", [])?;
|
||||||
|
|
||||||
// FIXME: read_host_result does not actually appear as an export!
|
// FIXME: read_host_result does not actually appear as an export!
|
||||||
// The interpreter has to look it up in debug info! (Apparently Wasm3 did this!)
|
// The interpreter has to look it up in debug info! (Apparently Wasm3 did this!)
|
||||||
// If we change gen_wasm to export it, then it does the same for js_unused,
|
// If we change gen_wasm to export it, then it does the same for js_unused,
|
||||||
// so we can't test import elimination and function reordering.
|
// so we can't test import elimination and function reordering.
|
||||||
// We should to come back to this and fix it.
|
// We should to come back to this and fix it.
|
||||||
inst.call_export(&module, "read_host_result", [])?
|
inst.call_export("read_host_result", [])?
|
||||||
.ok_or(String::from("expected a return value"))?
|
.ok_or(String::from("expected a return value"))?
|
||||||
.expect_i32()
|
.expect_i32()
|
||||||
.map_err(|type_err| format!("{:?}", type_err))
|
.map_err(|type_err| format!("{:?}", type_err))
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,15 @@ description = "Tests the roc REPL."
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
roc_cli = {path = "../cli"}
|
roc_cli = {path = "../cli"}
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "1.0.7"
|
indoc = "1.0.7"
|
||||||
strip-ansi-escapes = "0.1.1"
|
strip-ansi-escapes = "0.1.1"
|
||||||
wasmer-wasi = "2.2.1"
|
bumpalo.workspace = true
|
||||||
|
|
||||||
roc_build = { path = "../compiler/build" }
|
roc_build = { path = "../compiler/build" }
|
||||||
roc_repl_cli = {path = "../repl_cli"}
|
roc_repl_cli = {path = "../repl_cli"}
|
||||||
roc_test_utils = {path = "../test_utils"}
|
roc_test_utils = {path = "../test_utils"}
|
||||||
|
roc_wasm_interp = {path = "../wasm_interp"}
|
||||||
# Wasmer singlepass compiler only works on x86_64.
|
|
||||||
[target.'cfg(target_arch = "x86_64")'.dev-dependencies]
|
|
||||||
wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] }
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies]
|
|
||||||
wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["target-aarch64", "target-x86_64", "target-wasm32"]
|
default = ["target-aarch64", "target-x86_64", "target-wasm32"]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,3 @@
|
||||||
//! Tests the roc REPL.
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,289 +1,166 @@
|
||||||
use std::{
|
use bumpalo::Bump;
|
||||||
cell::RefCell,
|
use roc_wasm_interp::{
|
||||||
fs,
|
wasi, DefaultImportDispatcher, ImportDispatcher, Instance, Value, WasiDispatcher,
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
path::Path,
|
|
||||||
sync::Mutex,
|
|
||||||
thread_local,
|
|
||||||
};
|
};
|
||||||
use wasmer::{
|
|
||||||
imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value,
|
|
||||||
};
|
|
||||||
use wasmer_wasi::WasiState;
|
|
||||||
|
|
||||||
const WASM_REPL_COMPILER_PATH: &str = "../../target/wasm32-wasi/release/roc_repl_wasm.wasm";
|
const COMPILER_BYTES: &[u8] =
|
||||||
|
include_bytes!("../../../target/wasm32-wasi/release/roc_repl_wasm.wasm");
|
||||||
|
|
||||||
thread_local! {
|
struct CompilerDispatcher<'a> {
|
||||||
static REPL_STATE: RefCell<Option<ReplState>> = RefCell::new(None)
|
arena: &'a Bump,
|
||||||
|
src: &'a str,
|
||||||
|
answer: String,
|
||||||
|
wasi: WasiDispatcher<'a>,
|
||||||
|
app: Option<Instance<'a, DefaultImportDispatcher<'a>>>,
|
||||||
|
result_addr: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The compiler Wasm instance.
|
impl<'a> ImportDispatcher for CompilerDispatcher<'a> {
|
||||||
// This takes several *seconds* to initialise, so we only want to do it once for all tests.
|
fn dispatch(
|
||||||
// Every test mutates compiler memory in `unsafe` ways, so we run them sequentially using a Mutex.
|
&mut self,
|
||||||
// Even if Cargo uses many threads, these tests won't go any faster. But that's fine, they're quick.
|
module_name: &str,
|
||||||
lazy_static! {
|
function_name: &str,
|
||||||
static ref COMPILER: Instance = init_compiler();
|
arguments: &[Value],
|
||||||
}
|
compiler_memory: &mut [u8],
|
||||||
|
) -> Option<Value> {
|
||||||
static TEST_MUTEX: Mutex<()> = Mutex::new(());
|
let unknown = || {
|
||||||
|
panic!(
|
||||||
/// Load the compiler .wasm file and get it ready to execute
|
"I could not find an implementation for import {}.{}",
|
||||||
/// THIS FUNCTION TAKES 4 SECONDS TO RUN
|
module_name, function_name
|
||||||
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)),
|
|
||||||
};
|
|
||||||
println!("loaded Roc compiler bytes");
|
|
||||||
|
|
||||||
let store = Store::default();
|
|
||||||
|
|
||||||
// This is the slow line. Skipping validation checks reduces module compilation time from 5s to 4s.
|
|
||||||
// Safety: We trust rustc to produce a valid module.
|
|
||||||
let wasmer_module =
|
|
||||||
unsafe { Module::from_binary_unchecked(&store, &wasm_module_bytes).unwrap() };
|
|
||||||
|
|
||||||
// Specify the external functions the Wasm module needs to link to
|
|
||||||
// We only use WASI so that we can debug test failures more easily with println!(), dbg!(), etc.
|
|
||||||
let mut wasi_env = WasiState::new("compiler").finalize().unwrap();
|
|
||||||
let wasi_import_obj = wasi_env
|
|
||||||
.import_object(&wasmer_module)
|
|
||||||
.unwrap_or_else(|_| ImportObject::new());
|
|
||||||
let repl_import_obj = 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),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// "Chain" the import objects together. Wasmer will look up the REPL object first, then the WASI object
|
|
||||||
let import_object = wasi_import_obj.chain_front(repl_import_obj);
|
|
||||||
|
|
||||||
println!("Instantiating Roc compiler");
|
|
||||||
|
|
||||||
// Make a fully-linked instance with its own block of memory
|
|
||||||
let inst = Instance::new(&wasmer_module, &import_object).unwrap();
|
|
||||||
|
|
||||||
println!("Instantiated Roc compiler");
|
|
||||||
|
|
||||||
inst
|
|
||||||
}
|
|
||||||
|
|
||||||
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) -> u32 {
|
|
||||||
let app: Instance = {
|
|
||||||
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 = match Module::new(&store, app_module_bytes) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to create Wasm module\n{:?}", e);
|
|
||||||
if false {
|
|
||||||
let path = std::env::temp_dir().join("roc_repl_test_invalid_app.wasm");
|
|
||||||
fs::write(&path, app_module_bytes).unwrap();
|
|
||||||
println!("Wrote invalid wasm to {:?}", path);
|
|
||||||
}
|
|
||||||
return false.into();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the WASI imports for the app
|
if module_name == wasi::MODULE_NAME {
|
||||||
let mut wasi_env = WasiState::new("app").finalize().unwrap();
|
self.wasi
|
||||||
let import_object = wasi_env
|
.dispatch(function_name, arguments, compiler_memory)
|
||||||
.import_object(&wasmer_module)
|
} else if module_name == "env" {
|
||||||
.unwrap_or_else(|_| imports!());
|
match function_name {
|
||||||
|
"test_create_app" => {
|
||||||
|
// Get some bytes from the compiler Wasm instance and create the app Wasm instance
|
||||||
|
// fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32;
|
||||||
|
assert_eq!(arguments.len(), 2);
|
||||||
|
let app_bytes_ptr = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
let app_bytes_len = arguments[1].expect_i32().unwrap() as usize;
|
||||||
|
let app_bytes = &compiler_memory[app_bytes_ptr..][..app_bytes_len];
|
||||||
|
|
||||||
// Create an executable instance
|
let is_debug_mode = false;
|
||||||
match Instance::new(&wasmer_module, &import_object) {
|
let instance = Instance::from_bytes(
|
||||||
Ok(instance) => instance,
|
self.arena,
|
||||||
Err(e) => {
|
app_bytes,
|
||||||
println!("Failed to create Wasm instance {:?}", e);
|
DefaultImportDispatcher::default(),
|
||||||
return false.into();
|
is_debug_mode,
|
||||||
}
|
)
|
||||||
}
|
.unwrap();
|
||||||
};
|
|
||||||
|
|
||||||
REPL_STATE.with(|f| {
|
self.app = Some(instance);
|
||||||
if let Some(state) = f.borrow_mut().deref_mut() {
|
let ok = Value::I32(true as i32);
|
||||||
state.app = Some(app)
|
Some(ok)
|
||||||
} else {
|
}
|
||||||
unreachable!()
|
"test_run_app" => {
|
||||||
}
|
// fn test_run_app() -> usize;
|
||||||
});
|
assert_eq!(arguments.len(), 0);
|
||||||
|
match &mut self.app {
|
||||||
return true.into();
|
Some(instance) => {
|
||||||
}
|
let result_addr = instance
|
||||||
|
.call_export("wrapper", [])
|
||||||
fn wasmer_run_app() -> u32 {
|
.unwrap()
|
||||||
REPL_STATE.with(|f| {
|
.expect("No return address from wrapper")
|
||||||
if let Some(state) = f.borrow_mut().deref_mut() {
|
.expect_i32()
|
||||||
if let Some(app) = &state.app {
|
.unwrap();
|
||||||
let wrapper = app.exports.get_function("wrapper").unwrap();
|
self.result_addr = Some(result_addr);
|
||||||
|
let memory_size = instance.memory.len();
|
||||||
let result_addr: i32 = match wrapper.call(&[]) {
|
Some(Value::I32(memory_size as i32))
|
||||||
Err(e) => panic!("{:?}", e),
|
}
|
||||||
Ok(result) => result[0].unwrap_i32(),
|
None => panic!("Trying to run the app but it hasn't been created"),
|
||||||
};
|
}
|
||||||
state.result_addr = Some(result_addr as u32);
|
}
|
||||||
|
"test_get_result_and_memory" => {
|
||||||
let memory = app.exports.get_memory("memory").unwrap();
|
// Copy the app's entire memory buffer into the compiler's memory,
|
||||||
memory.size().bytes().0 as u32
|
// and return the location in that buffer where we can find the app result.
|
||||||
} else {
|
// fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
|
||||||
unreachable!()
|
assert_eq!(arguments.len(), 1);
|
||||||
|
let buffer_alloc_addr = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
match &self.app {
|
||||||
|
Some(instance) => {
|
||||||
|
let len = instance.memory.len();
|
||||||
|
compiler_memory[buffer_alloc_addr..][..len]
|
||||||
|
.copy_from_slice(&instance.memory);
|
||||||
|
self.result_addr.map(Value::I32)
|
||||||
|
}
|
||||||
|
None => panic!("Trying to get result and memory but there is no app"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"test_copy_input_string" => {
|
||||||
|
// Copy the Roc source code from the test into the compiler Wasm instance
|
||||||
|
// fn test_copy_input_string(src_buffer_addr: *mut u8);
|
||||||
|
assert_eq!(arguments.len(), 1);
|
||||||
|
let src_buffer_addr = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
let len = self.src.len();
|
||||||
|
compiler_memory[src_buffer_addr..][..len].copy_from_slice(self.src.as_bytes());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"test_copy_output_string" => {
|
||||||
|
// The REPL now has a string representing the answer. Make it available to the test code.
|
||||||
|
// fn test_copy_output_string(output_ptr: *const u8, output_len: usize);
|
||||||
|
assert_eq!(arguments.len(), 2);
|
||||||
|
let output_ptr = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
let output_len = arguments[1].expect_i32().unwrap() as usize;
|
||||||
|
match std::str::from_utf8(&compiler_memory[output_ptr..][..output_len]) {
|
||||||
|
Ok(answer) => {
|
||||||
|
self.answer = answer.into();
|
||||||
|
}
|
||||||
|
Err(e) => panic!("{:?}", e),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"now" => Some(Value::F64(0.0)),
|
||||||
|
_ => unknown(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unknown()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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!()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wasmer_copy_output_string(output_ptr: u32, output_len: u32) {
|
|
||||||
let output: String = {
|
|
||||||
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) }
|
|
||||||
};
|
|
||||||
|
|
||||||
REPL_STATE.with(|f| {
|
|
||||||
if let Some(state) = f.borrow_mut().deref_mut() {
|
|
||||||
state.output = Some(output)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_compiler_load_error(e: std::io::Error) -> String {
|
|
||||||
if matches!(e.kind(), std::io::ErrorKind::NotFound) {
|
|
||||||
format!(
|
|
||||||
"\n\n {}\n\n",
|
|
||||||
[
|
|
||||||
"ROC COMPILER WASM BINARY NOT FOUND",
|
|
||||||
"Please run these tests using repl_test/run_wasm.sh!",
|
|
||||||
"It will build a .wasm binary for the compiler, and a native binary for the tests themselves",
|
|
||||||
]
|
|
||||||
.join("\n ")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("{:?}", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy_system_time_now() -> f64 {
|
fn run(src: &'static str) -> Result<String, String> {
|
||||||
0.0
|
let arena = Bump::new();
|
||||||
}
|
|
||||||
|
|
||||||
fn run(src: &'static str) -> (bool, String) {
|
let mut instance = {
|
||||||
println!("run");
|
let dispatcher = CompilerDispatcher {
|
||||||
REPL_STATE.with(|rs| {
|
arena: &arena,
|
||||||
*rs.borrow_mut().deref_mut() = Some(ReplState {
|
|
||||||
src,
|
src,
|
||||||
|
answer: String::new(),
|
||||||
|
wasi: WasiDispatcher::default(),
|
||||||
app: None,
|
app: None,
|
||||||
result_addr: None,
|
result_addr: None,
|
||||||
output: None,
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let ok = if let Ok(_guard) = TEST_MUTEX.lock() {
|
let is_debug_mode = false; // logs every instruction!
|
||||||
let entrypoint = COMPILER
|
Instance::from_bytes(&arena, COMPILER_BYTES, dispatcher, is_debug_mode).unwrap()
|
||||||
.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 {
|
|
||||||
panic!(
|
|
||||||
"Failed to acquire test mutex! A previous test must have panicked while holding it, running Wasm"
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_state: ReplState = REPL_STATE.with(|rs| rs.take()).unwrap();
|
let len = Value::I32(src.len() as i32);
|
||||||
let output: String = final_state.output.unwrap();
|
let wasm_ok: i32 = instance
|
||||||
|
.call_export("entrypoint_from_test", [len])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.expect_i32()
|
||||||
|
.unwrap();
|
||||||
|
let answer_str = instance.import_dispatcher.answer.to_owned();
|
||||||
|
|
||||||
(ok, output)
|
if wasm_ok == 0 {
|
||||||
|
Err(answer_str)
|
||||||
|
} else {
|
||||||
|
Ok(answer_str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn expect_success(input: &'static str, expected: &str) {
|
pub fn expect_success(input: &'static str, expected: &str) {
|
||||||
let (ok, output) = run(input);
|
assert_eq!(run(input), Ok(expected.into()));
|
||||||
if !ok {
|
|
||||||
panic!("\n{}\n", output);
|
|
||||||
}
|
|
||||||
assert_eq!(output, expected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn expect_failure(input: &'static str, expected: &str) {
|
pub fn expect_failure(input: &'static str, expected: &str) {
|
||||||
let (ok, output) = run(input);
|
assert_eq!(run(input), Err(expected.into()));
|
||||||
assert_eq!(ok, false);
|
|
||||||
assert_eq!(output, expected);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ set -euxo pipefail
|
||||||
|
|
||||||
# We need to clear RUSTFLAGS for this command, as CI sets normally some flags that are specific to CPU targets.
|
# We need to clear RUSTFLAGS for this command, as CI sets normally some flags that are specific to CPU targets.
|
||||||
# Tests target wasm32-wasi instead of wasm32-unknown-unknown, so that we can debug with println! and dbg!
|
# Tests target wasm32-wasi instead of wasm32-unknown-unknown, so that we can debug with println! and dbg!
|
||||||
RUSTFLAGS="" cargo build --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasmer --release
|
RUSTFLAGS="" cargo build --locked --release --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasi_test
|
||||||
|
|
||||||
# Build & run the test code on *native* target, not WebAssembly
|
# Build & run the test code on *native* target, not WebAssembly
|
||||||
cargo test -p repl_test --features wasm -- --test-threads=1
|
cargo test --locked --release -p repl_test --features wasm
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ roc_target = {path = "../compiler/roc_target"}
|
||||||
roc_types = {path = "../compiler/types"}
|
roc_types = {path = "../compiler/types"}
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
wasmer = ["futures"]
|
wasi_test = ["futures"]
|
||||||
|
|
||||||
# Tell wasm-pack not to run wasm-opt automatically. We run it explicitly when we need to.
|
# Tell wasm-pack not to run wasm-opt automatically. We run it explicitly when we need to.
|
||||||
# (Workaround for a CI install issue with wasm-pack https://github.com/rustwasm/wasm-pack/issues/864)
|
# (Workaround for a CI install issue with wasm-pack https://github.com/rustwasm/wasm-pack/issues/864)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ extern "C" {
|
||||||
|
|
||||||
// To debug in the browser, start up the web REPL as per instructions in repl_www/README.md
|
// To debug in the browser, start up the web REPL as per instructions in repl_www/README.md
|
||||||
// and sprinkle your code with console_log!("{:?}", my_value);
|
// and sprinkle your code with console_log!("{:?}", my_value);
|
||||||
// (Or if you're running the unit tests in Wasmer, you can just use println! or dbg!)
|
// (Or if you're running the unit tests with WASI, you can just use println! or dbg!)
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! console_log {
|
macro_rules! console_log {
|
||||||
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
use futures::executor;
|
use futures::executor;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn wasmer_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32;
|
fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32;
|
||||||
fn wasmer_run_app() -> usize;
|
fn test_run_app() -> usize;
|
||||||
fn wasmer_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
|
fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
|
||||||
fn wasmer_copy_input_string(src_buffer_addr: *mut u8);
|
fn test_copy_input_string(src_buffer_addr: *mut u8);
|
||||||
fn wasmer_copy_output_string(output_ptr: *const u8, output_len: usize);
|
fn test_copy_output_string(output_ptr: *const u8, output_len: usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Async wrapper to match the equivalent JS function
|
/// Async wrapper to match the equivalent JS function
|
||||||
pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> {
|
pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> {
|
||||||
let ok = unsafe { wasmer_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()) } != 0;
|
let ok = unsafe { test_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()) } != 0;
|
||||||
if ok {
|
if ok {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -19,22 +19,21 @@ pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn js_run_app() -> usize {
|
pub fn js_run_app() -> usize {
|
||||||
unsafe { wasmer_run_app() }
|
unsafe { test_run_app() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize {
|
pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize {
|
||||||
unsafe { wasmer_get_result_and_memory(buffer_alloc_addr) }
|
unsafe { test_get_result_and_memory(buffer_alloc_addr) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Entrypoint for Wasmer tests
|
/// Entrypoint for tests using WASI and a CLI interpreter
|
||||||
/// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary.
|
/// - 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 (in the browser version, wasm_bindgen does this)
|
||||||
/// - Uses an extra callback to allocate & copy the input string (wasm_bindgen does this for JS)
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool {
|
pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool {
|
||||||
let mut src_buffer = std::vec![0; src_len];
|
let mut src_buffer = std::vec![0; src_len];
|
||||||
let src = unsafe {
|
let src = unsafe {
|
||||||
wasmer_copy_input_string(src_buffer.as_mut_ptr());
|
test_copy_input_string(src_buffer.as_mut_ptr());
|
||||||
String::from_utf8_unchecked(src_buffer)
|
String::from_utf8_unchecked(src_buffer)
|
||||||
};
|
};
|
||||||
let result_async = crate::repl::entrypoint_from_js(src);
|
let result_async = crate::repl::entrypoint_from_js(src);
|
||||||
|
|
@ -43,7 +42,7 @@ pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool {
|
||||||
|
|
||||||
let output = result.unwrap_or_else(|s| s);
|
let output = result.unwrap_or_else(|s| s);
|
||||||
|
|
||||||
unsafe { wasmer_copy_output_string(output.as_ptr(), output.len()) }
|
unsafe { test_copy_output_string(output.as_ptr(), output.len()) }
|
||||||
|
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ mod repl;
|
||||||
//
|
//
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
extern crate console_error_panic_hook;
|
extern crate console_error_panic_hook;
|
||||||
#[cfg(not(feature = "wasmer"))]
|
#[cfg(not(feature = "wasi_test"))]
|
||||||
mod externs_js;
|
mod externs_js;
|
||||||
#[cfg(not(feature = "wasmer"))]
|
#[cfg(not(feature = "wasi_test"))]
|
||||||
pub use externs_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app};
|
pub use externs_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Interface with test code outside the Wasm module
|
// Interface with test code outside the Wasm module
|
||||||
//
|
//
|
||||||
#[cfg(feature = "wasmer")]
|
#[cfg(feature = "wasi_test")]
|
||||||
mod externs_test;
|
mod externs_test;
|
||||||
#[cfg(feature = "wasmer")]
|
#[cfg(feature = "wasi_test")]
|
||||||
pub use externs_test::{entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app};
|
pub use externs_test::{entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
roc_wasm_module = { path = "../wasm_module" }
|
roc_wasm_module = { path = "../wasm_module" }
|
||||||
|
rand = "0.8.4"
|
||||||
bitvec.workspace = true
|
bitvec.workspace = true
|
||||||
bumpalo.workspace = true
|
bumpalo.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ enum Block {
|
||||||
Normal { vstack: usize },
|
Normal { vstack: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
struct BranchCacheEntry {
|
struct BranchCacheEntry {
|
||||||
addr: u32,
|
addr: u32,
|
||||||
argument: u32,
|
argument: u32,
|
||||||
|
|
@ -33,6 +33,7 @@ struct BranchCacheEntry {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Instance<'a, I: ImportDispatcher> {
|
pub struct Instance<'a, I: ImportDispatcher> {
|
||||||
|
module: &'a WasmModule<'a>,
|
||||||
/// Contents of the WebAssembly instance's memory
|
/// Contents of the WebAssembly instance's memory
|
||||||
pub memory: Vec<'a, u8>,
|
pub memory: Vec<'a, u8>,
|
||||||
/// Metadata for every currently-active function call
|
/// Metadata for every currently-active function call
|
||||||
|
|
@ -47,10 +48,14 @@ pub struct Instance<'a, I: ImportDispatcher> {
|
||||||
blocks: Vec<'a, Block>,
|
blocks: Vec<'a, Block>,
|
||||||
/// Outermost block depth for the currently-executing function.
|
/// Outermost block depth for the currently-executing function.
|
||||||
outermost_block: u32,
|
outermost_block: u32,
|
||||||
/// Cache for branching instructions
|
/// Current function index
|
||||||
branch_cache: Vec<'a, BranchCacheEntry>,
|
current_function: usize,
|
||||||
|
/// Cache for branching instructions, split into buckets for each function.
|
||||||
|
branch_cache: Vec<'a, Vec<'a, BranchCacheEntry>>,
|
||||||
|
/// Number of imports in the module
|
||||||
|
import_count: usize,
|
||||||
/// Import dispatcher from user code
|
/// Import dispatcher from user code
|
||||||
import_dispatcher: I,
|
pub import_dispatcher: I,
|
||||||
/// Temporary storage for import arguments
|
/// Temporary storage for import arguments
|
||||||
import_arguments: Vec<'a, Value>,
|
import_arguments: Vec<'a, Value>,
|
||||||
/// temporary storage for output using the --debug option
|
/// temporary storage for output using the --debug option
|
||||||
|
|
@ -58,7 +63,8 @@ pub struct Instance<'a, I: ImportDispatcher> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
pub fn new<G>(
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new<G>(
|
||||||
arena: &'a Bump,
|
arena: &'a Bump,
|
||||||
memory_pages: u32,
|
memory_pages: u32,
|
||||||
program_counter: usize,
|
program_counter: usize,
|
||||||
|
|
@ -70,6 +76,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
{
|
{
|
||||||
let mem_bytes = memory_pages * MemorySection::PAGE_SIZE;
|
let mem_bytes = memory_pages * MemorySection::PAGE_SIZE;
|
||||||
Instance {
|
Instance {
|
||||||
|
module: arena.alloc(WasmModule::new(arena)),
|
||||||
memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena),
|
memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena),
|
||||||
call_stack: CallStack::new(arena),
|
call_stack: CallStack::new(arena),
|
||||||
value_stack: ValueStack::new(arena),
|
value_stack: ValueStack::new(arena),
|
||||||
|
|
@ -77,16 +84,29 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
program_counter,
|
program_counter,
|
||||||
blocks: Vec::new_in(arena),
|
blocks: Vec::new_in(arena),
|
||||||
outermost_block: 0,
|
outermost_block: 0,
|
||||||
branch_cache: Vec::new_in(arena),
|
branch_cache: bumpalo::vec![in arena; bumpalo::vec![in arena]],
|
||||||
|
current_function: 0,
|
||||||
|
import_count: 0,
|
||||||
import_dispatcher,
|
import_dispatcher,
|
||||||
import_arguments: Vec::new_in(arena),
|
import_arguments: Vec::new_in(arena),
|
||||||
debug_string: Some(String::new()),
|
debug_string: Some(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(
|
||||||
|
arena: &'a Bump,
|
||||||
|
module_bytes: &[u8],
|
||||||
|
import_dispatcher: I,
|
||||||
|
is_debug_mode: bool,
|
||||||
|
) -> Result<Self, std::string::String> {
|
||||||
|
let module =
|
||||||
|
WasmModule::preload(arena, module_bytes, false).map_err(|e| format!("{:?}", e))?;
|
||||||
|
Self::for_module(arena, arena.alloc(module), import_dispatcher, is_debug_mode)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn for_module(
|
pub fn for_module(
|
||||||
arena: &'a Bump,
|
arena: &'a Bump,
|
||||||
module: &WasmModule<'a>,
|
module: &'a WasmModule<'a>,
|
||||||
import_dispatcher: I,
|
import_dispatcher: I,
|
||||||
is_debug_mode: bool,
|
is_debug_mode: bool,
|
||||||
) -> Result<Self, std::string::String> {
|
) -> Result<Self, std::string::String> {
|
||||||
|
|
@ -118,7 +138,15 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let import_count = module.import.imports.len();
|
||||||
|
let branch_cache = {
|
||||||
|
let num_functions = import_count + module.code.function_count as usize;
|
||||||
|
let empty_caches_iter = iter::repeat(Vec::new_in(arena)).take(num_functions);
|
||||||
|
Vec::from_iter_in(empty_caches_iter, arena)
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Instance {
|
Ok(Instance {
|
||||||
|
module,
|
||||||
memory,
|
memory,
|
||||||
call_stack,
|
call_stack,
|
||||||
value_stack,
|
value_stack,
|
||||||
|
|
@ -126,23 +154,20 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
program_counter: usize::MAX,
|
program_counter: usize::MAX,
|
||||||
blocks: Vec::new_in(arena),
|
blocks: Vec::new_in(arena),
|
||||||
outermost_block: 0,
|
outermost_block: 0,
|
||||||
branch_cache: Vec::new_in(arena),
|
current_function: usize::MAX,
|
||||||
|
branch_cache,
|
||||||
|
import_count,
|
||||||
import_dispatcher,
|
import_dispatcher,
|
||||||
import_arguments: Vec::new_in(arena),
|
import_arguments: Vec::new_in(arena),
|
||||||
debug_string,
|
debug_string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_export<A>(
|
pub fn call_export<A>(&mut self, fn_name: &str, arg_values: A) -> Result<Option<Value>, String>
|
||||||
&mut self,
|
|
||||||
module: &WasmModule<'a>,
|
|
||||||
fn_name: &str,
|
|
||||||
arg_values: A,
|
|
||||||
) -> Result<Option<Value>, String>
|
|
||||||
where
|
where
|
||||||
A: IntoIterator<Item = Value>,
|
A: IntoIterator<Item = Value>,
|
||||||
{
|
{
|
||||||
let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?;
|
let arg_type_bytes = self.prepare_to_call_export(self.module, fn_name)?;
|
||||||
|
|
||||||
for (i, (value, type_byte)) in arg_values
|
for (i, (value, type_byte)) in arg_values
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -160,14 +185,14 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
self.value_stack.push(value);
|
self.value_stack.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.call_export_help(module, arg_type_bytes)
|
self.call_export_help(self.module, arg_type_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_export_from_cli(
|
pub fn call_export_from_cli(
|
||||||
&mut self,
|
&mut self,
|
||||||
module: &WasmModule<'a>,
|
module: &WasmModule<'a>,
|
||||||
fn_name: &str,
|
fn_name: &str,
|
||||||
arg_strings: &'a [&'a String],
|
arg_strings: &'a [&'a [u8]],
|
||||||
) -> Result<Option<Value>, String> {
|
) -> Result<Option<Value>, String> {
|
||||||
// We have two different mechanisms for handling CLI arguments!
|
// We have two different mechanisms for handling CLI arguments!
|
||||||
// 1. Basic numbers:
|
// 1. Basic numbers:
|
||||||
|
|
@ -183,12 +208,13 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
// Implement the "basic numbers" CLI
|
// Implement the "basic numbers" CLI
|
||||||
// Check if the called Wasm function takes numeric arguments, and if so, try to parse them from the CLI.
|
// Check if the called Wasm function takes numeric arguments, and if so, try to parse them from the CLI.
|
||||||
let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?;
|
let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?;
|
||||||
for (value_str, type_byte) in arg_strings
|
for (value_bytes, type_byte) in arg_strings
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1) // first string is the .wasm filename
|
.skip(1) // first string is the .wasm filename
|
||||||
.zip(arg_type_bytes.iter().copied())
|
.zip(arg_type_bytes.iter().copied())
|
||||||
{
|
{
|
||||||
use ValueType::*;
|
use ValueType::*;
|
||||||
|
let value_str = String::from_utf8_lossy(value_bytes);
|
||||||
let value = match ValueType::from(type_byte) {
|
let value = match ValueType::from(type_byte) {
|
||||||
I32 => Value::I32(value_str.parse::<i32>().map_err(|e| e.to_string())?),
|
I32 => Value::I32(value_str.parse::<i32>().map_err(|e| e.to_string())?),
|
||||||
I64 => Value::I64(value_str.parse::<i64>().map_err(|e| e.to_string())?),
|
I64 => Value::I64(value_str.parse::<i64>().map_err(|e| e.to_string())?),
|
||||||
|
|
@ -206,7 +232,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
module: &'m WasmModule<'a>,
|
module: &'m WasmModule<'a>,
|
||||||
fn_name: &str,
|
fn_name: &str,
|
||||||
) -> Result<&'m [u8], String> {
|
) -> Result<&'m [u8], String> {
|
||||||
let fn_index = {
|
self.current_function = {
|
||||||
let mut export_iter = module.export.exports.iter();
|
let mut export_iter = module.export.exports.iter();
|
||||||
export_iter
|
export_iter
|
||||||
// First look up the name in exports
|
// First look up the name in exports
|
||||||
|
|
@ -237,18 +263,18 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
"I couldn't find a function '{}' in this WebAssembly module",
|
"I couldn't find a function '{}' in this WebAssembly module",
|
||||||
fn_name
|
fn_name
|
||||||
)
|
)
|
||||||
})?
|
})? as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let internal_fn_index = self.current_function - self.import_count;
|
||||||
|
|
||||||
self.program_counter = {
|
self.program_counter = {
|
||||||
let internal_fn_index = fn_index as usize - module.import.function_count();
|
|
||||||
let mut cursor = module.code.function_offsets[internal_fn_index] as usize;
|
let mut cursor = module.code.function_offsets[internal_fn_index] as usize;
|
||||||
let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor);
|
let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor);
|
||||||
cursor
|
cursor
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg_type_bytes = {
|
let arg_type_bytes = {
|
||||||
let internal_fn_index = fn_index as usize - module.import.imports.len();
|
|
||||||
let signature_index = module.function.signatures[internal_fn_index];
|
let signature_index = module.function.signatures[internal_fn_index];
|
||||||
module.types.look_up_arg_type_bytes(signature_index)
|
module.types.look_up_arg_type_bytes(signature_index)
|
||||||
};
|
};
|
||||||
|
|
@ -256,7 +282,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
if self.debug_string.is_some() {
|
if self.debug_string.is_some() {
|
||||||
println!(
|
println!(
|
||||||
"Calling export func[{}] '{}' at address {:#x}",
|
"Calling export func[{}] '{}' at address {:#x}",
|
||||||
fn_index,
|
self.current_function,
|
||||||
fn_name,
|
fn_name,
|
||||||
self.program_counter + module.code.section_offset as usize
|
self.program_counter + module.code.section_offset as usize
|
||||||
);
|
);
|
||||||
|
|
@ -385,8 +411,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
use OpCode::*;
|
use OpCode::*;
|
||||||
|
|
||||||
let addr = self.program_counter as u32;
|
let addr = self.program_counter as u32;
|
||||||
let cache_result = self
|
let cache_result = self.branch_cache[self.current_function]
|
||||||
.branch_cache
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| entry.addr == addr && entry.argument == relative_blocks_outward);
|
.find(|entry| entry.addr == addr && entry.argument == relative_blocks_outward);
|
||||||
|
|
||||||
|
|
@ -412,7 +437,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.branch_cache.push(BranchCacheEntry {
|
self.branch_cache[self.current_function].push(BranchCacheEntry {
|
||||||
addr,
|
addr,
|
||||||
argument: relative_blocks_outward,
|
argument: relative_blocks_outward,
|
||||||
target: self.program_counter as u32,
|
target: self.program_counter as u32,
|
||||||
|
|
@ -427,9 +452,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
fn_index: usize,
|
fn_index: usize,
|
||||||
module: &WasmModule<'a>,
|
module: &WasmModule<'a>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let n_import_fns = module.import.imports.len();
|
let (signature_index, opt_import) = if fn_index < self.import_count {
|
||||||
|
|
||||||
let (signature_index, opt_import) = if fn_index < n_import_fns {
|
|
||||||
// Imported non-Wasm function
|
// Imported non-Wasm function
|
||||||
let import = &module.import.imports[fn_index];
|
let import = &module.import.imports[fn_index];
|
||||||
let sig = match import.description {
|
let sig = match import.description {
|
||||||
|
|
@ -439,7 +462,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
(sig, Some(import))
|
(sig, Some(import))
|
||||||
} else {
|
} else {
|
||||||
// Wasm function
|
// Wasm function
|
||||||
let sig = module.function.signatures[fn_index - n_import_fns];
|
let sig = module.function.signatures[fn_index - self.import_count];
|
||||||
(sig, None)
|
(sig, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -477,7 +500,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let return_addr = self.program_counter as u32;
|
let return_addr = self.program_counter as u32;
|
||||||
let internal_fn_index = fn_index - n_import_fns;
|
let internal_fn_index = fn_index - self.import_count;
|
||||||
self.program_counter = module.code.function_offsets[internal_fn_index] as usize;
|
self.program_counter = module.code.function_offsets[internal_fn_index] as usize;
|
||||||
|
|
||||||
let return_block_depth = self.outermost_block;
|
let return_block_depth = self.outermost_block;
|
||||||
|
|
@ -494,6 +517,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
&mut self.program_counter,
|
&mut self.program_counter,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
self.current_function = fn_index;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,7 +565,9 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
});
|
});
|
||||||
if condition == 0 {
|
if condition == 0 {
|
||||||
let addr = self.program_counter as u32;
|
let addr = self.program_counter as u32;
|
||||||
let cache_result = self.branch_cache.iter().find(|entry| entry.addr == addr);
|
let cache_result = self.branch_cache[self.current_function]
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.addr == addr);
|
||||||
if let Some(entry) = cache_result {
|
if let Some(entry) = cache_result {
|
||||||
self.program_counter = entry.target as usize;
|
self.program_counter = entry.target as usize;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -572,7 +598,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.branch_cache.push(BranchCacheEntry {
|
self.branch_cache[self.current_function].push(BranchCacheEntry {
|
||||||
addr,
|
addr,
|
||||||
argument: 0,
|
argument: 0,
|
||||||
target: self.program_counter as u32,
|
target: self.program_counter as u32,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ pub mod wasi;
|
||||||
|
|
||||||
// Main external interface
|
// Main external interface
|
||||||
pub use instance::Instance;
|
pub use instance::Instance;
|
||||||
pub use wasi::WasiDispatcher;
|
pub use wasi::{WasiDispatcher, WasiFile};
|
||||||
|
|
||||||
use roc_wasm_module::{Value, ValueType, WasmModule};
|
pub use roc_wasm_module::Value;
|
||||||
|
use roc_wasm_module::{ValueType, WasmModule};
|
||||||
use value_stack::ValueStack;
|
use value_stack::ValueStack;
|
||||||
|
|
||||||
pub trait ImportDispatcher {
|
pub trait ImportDispatcher {
|
||||||
|
|
@ -22,18 +23,22 @@ pub trait ImportDispatcher {
|
||||||
) -> Option<Value>;
|
) -> Option<Value>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_IMPORTS: DefaultImportDispatcher = DefaultImportDispatcher {
|
impl Default for DefaultImportDispatcher<'_> {
|
||||||
wasi: WasiDispatcher { args: &[] },
|
fn default() -> Self {
|
||||||
};
|
DefaultImportDispatcher {
|
||||||
|
wasi: WasiDispatcher::new(&[]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DefaultImportDispatcher<'a> {
|
pub struct DefaultImportDispatcher<'a> {
|
||||||
wasi: WasiDispatcher<'a>,
|
pub wasi: WasiDispatcher<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DefaultImportDispatcher<'a> {
|
impl<'a> DefaultImportDispatcher<'a> {
|
||||||
pub fn new(args: &'a [&'a String]) -> Self {
|
pub fn new(args: &'a [&'a [u8]]) -> Self {
|
||||||
DefaultImportDispatcher {
|
DefaultImportDispatcher {
|
||||||
wasi: WasiDispatcher { args },
|
wasi: WasiDispatcher::new(args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,10 @@ fn main() -> io::Result<()> {
|
||||||
let start_arg_strings = matches.get_many::<String>(ARGS_FOR_APP).unwrap_or_default();
|
let start_arg_strings = matches.get_many::<String>(ARGS_FOR_APP).unwrap_or_default();
|
||||||
let wasm_path = matches.get_one::<String>(WASM_FILE).unwrap();
|
let wasm_path = matches.get_one::<String>(WASM_FILE).unwrap();
|
||||||
// WASI expects the .wasm file to be argv[0]
|
// WASI expects the .wasm file to be argv[0]
|
||||||
let wasi_argv = Vec::from_iter_in(once(wasm_path).chain(start_arg_strings), &arena);
|
let wasi_argv_iter = once(wasm_path)
|
||||||
|
.chain(start_arg_strings)
|
||||||
|
.map(|s| s.as_bytes());
|
||||||
|
let wasi_argv = Vec::from_iter_in(wasi_argv_iter, &arena);
|
||||||
|
|
||||||
// Load the WebAssembly binary file
|
// Load the WebAssembly binary file
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ mod test_i32;
|
||||||
mod test_i64;
|
mod test_i64;
|
||||||
mod test_mem;
|
mod test_mem;
|
||||||
|
|
||||||
use crate::{DefaultImportDispatcher, Instance, DEFAULT_IMPORTS};
|
use crate::{DefaultImportDispatcher, Instance};
|
||||||
use bumpalo::{collections::Vec, Bump};
|
use bumpalo::{collections::Vec, Bump};
|
||||||
use roc_wasm_module::{
|
use roc_wasm_module::{
|
||||||
opcodes::OpCode, Export, ExportType, SerialBuffer, Signature, Value, ValueType, WasmModule,
|
opcodes::OpCode, Export, ExportType, SerialBuffer, Signature, Value, ValueType, WasmModule,
|
||||||
|
|
@ -18,7 +18,13 @@ pub fn default_state(arena: &Bump) -> Instance<DefaultImportDispatcher> {
|
||||||
let pages = 1;
|
let pages = 1;
|
||||||
let program_counter = 0;
|
let program_counter = 0;
|
||||||
let globals = [];
|
let globals = [];
|
||||||
Instance::new(arena, pages, program_counter, globals, DEFAULT_IMPORTS)
|
Instance::new(
|
||||||
|
arena,
|
||||||
|
pages,
|
||||||
|
program_counter,
|
||||||
|
globals,
|
||||||
|
DefaultImportDispatcher::default(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn const_value(buf: &mut Vec<'_, u8>, value: Value) {
|
pub fn const_value(buf: &mut Vec<'_, u8>, value: Value) {
|
||||||
|
|
@ -85,9 +91,10 @@ where
|
||||||
std::fs::write(&filename, outfile_buf).unwrap();
|
std::fs::write(&filename, outfile_buf).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, true).unwrap();
|
let mut inst =
|
||||||
|
Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), false).unwrap();
|
||||||
|
|
||||||
let return_val = inst.call_export(&module, "test", []).unwrap().unwrap();
|
let return_val = inst.call_export("test", []).unwrap().unwrap();
|
||||||
|
|
||||||
assert_eq!(return_val, expected);
|
assert_eq!(return_val, expected);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#![cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use super::{const_value, create_exported_function_no_locals, default_state};
|
use super::{const_value, create_exported_function_no_locals, default_state};
|
||||||
use crate::{instance::Action, ImportDispatcher, Instance, ValueStack, DEFAULT_IMPORTS};
|
use crate::{instance::Action, DefaultImportDispatcher, ImportDispatcher, Instance, ValueStack};
|
||||||
use bumpalo::{collections::Vec, Bump};
|
use bumpalo::{collections::Vec, Bump};
|
||||||
use roc_wasm_module::sections::{Import, ImportDesc};
|
use roc_wasm_module::sections::{Import, ImportDesc};
|
||||||
use roc_wasm_module::{
|
use roc_wasm_module::{
|
||||||
|
|
@ -554,10 +554,7 @@ fn test_call_import() {
|
||||||
|
|
||||||
let mut inst = Instance::for_module(&arena, &module, import_dispatcher, true).unwrap();
|
let mut inst = Instance::for_module(&arena, &module, import_dispatcher, true).unwrap();
|
||||||
|
|
||||||
let return_val = inst
|
let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap();
|
||||||
.call_export(&module, start_fn_name, [])
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(return_val, Value::I32(234));
|
assert_eq!(return_val, Value::I32(234));
|
||||||
}
|
}
|
||||||
|
|
@ -623,12 +620,10 @@ fn test_call_return_no_args() {
|
||||||
println!("Wrote to {}", filename);
|
println!("Wrote to {}", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, true).unwrap();
|
let mut inst =
|
||||||
|
Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), true).unwrap();
|
||||||
|
|
||||||
let return_val = inst
|
let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap();
|
||||||
.call_export(&module, start_fn_name, [])
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(return_val, Value::I32(42));
|
assert_eq!(return_val, Value::I32(42));
|
||||||
}
|
}
|
||||||
|
|
@ -762,10 +757,14 @@ fn test_call_indirect_help(table_index: u32, elem_index: u32) -> Value {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, is_debug_mode).unwrap();
|
let mut inst = Instance::for_module(
|
||||||
inst.call_export(&module, start_fn_name, [])
|
&arena,
|
||||||
.unwrap()
|
&module,
|
||||||
.unwrap()
|
DefaultImportDispatcher::default(),
|
||||||
|
is_debug_mode,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
inst.call_export(start_fn_name, []).unwrap().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[test]
|
// #[test]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::create_exported_function_no_locals;
|
use super::create_exported_function_no_locals;
|
||||||
use crate::{Instance, DEFAULT_IMPORTS};
|
use crate::{DefaultImportDispatcher, Instance};
|
||||||
use bumpalo::{collections::Vec, Bump};
|
use bumpalo::{collections::Vec, Bump};
|
||||||
use roc_wasm_module::{
|
use roc_wasm_module::{
|
||||||
opcodes::OpCode,
|
opcodes::OpCode,
|
||||||
|
|
@ -18,7 +18,7 @@ fn test_currentmemory() {
|
||||||
module.code.bytes.push(OpCode::CURRENTMEMORY as u8);
|
module.code.bytes.push(OpCode::CURRENTMEMORY as u8);
|
||||||
module.code.bytes.encode_i32(0);
|
module.code.bytes.encode_i32(0);
|
||||||
|
|
||||||
let mut state = Instance::new(&arena, pages, pc, [], DEFAULT_IMPORTS);
|
let mut state = Instance::new(&arena, pages, pc, [], DefaultImportDispatcher::default());
|
||||||
state.execute_next_instruction(&module).unwrap();
|
state.execute_next_instruction(&module).unwrap();
|
||||||
assert_eq!(state.value_stack.pop(), Value::I32(3))
|
assert_eq!(state.value_stack.pop(), Value::I32(3))
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +37,13 @@ fn test_growmemory() {
|
||||||
module.code.bytes.push(OpCode::GROWMEMORY as u8);
|
module.code.bytes.push(OpCode::GROWMEMORY as u8);
|
||||||
module.code.bytes.encode_i32(0);
|
module.code.bytes.encode_i32(0);
|
||||||
|
|
||||||
let mut state = Instance::new(&arena, existing_pages, pc, [], DEFAULT_IMPORTS);
|
let mut state = Instance::new(
|
||||||
|
&arena,
|
||||||
|
existing_pages,
|
||||||
|
pc,
|
||||||
|
[],
|
||||||
|
DefaultImportDispatcher::default(),
|
||||||
|
);
|
||||||
state.execute_next_instruction(&module).unwrap();
|
state.execute_next_instruction(&module).unwrap();
|
||||||
state.execute_next_instruction(&module).unwrap();
|
state.execute_next_instruction(&module).unwrap();
|
||||||
assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize);
|
assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize);
|
||||||
|
|
@ -79,10 +85,14 @@ fn test_load(load_op: OpCode, ty: ValueType, data: &[u8], addr: u32, offset: u32
|
||||||
std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap();
|
std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, is_debug_mode).unwrap();
|
let mut inst = Instance::for_module(
|
||||||
inst.call_export(&module, start_fn_name, [])
|
&arena,
|
||||||
.unwrap()
|
&module,
|
||||||
.unwrap()
|
DefaultImportDispatcher::default(),
|
||||||
|
is_debug_mode,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
inst.call_export(start_fn_name, []).unwrap().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -233,7 +243,7 @@ fn test_i64load32u() {
|
||||||
|
|
||||||
fn test_store<'a>(
|
fn test_store<'a>(
|
||||||
arena: &'a Bump,
|
arena: &'a Bump,
|
||||||
module: &mut WasmModule<'a>,
|
module: &'a mut WasmModule<'a>,
|
||||||
addr: u32,
|
addr: u32,
|
||||||
store_op: OpCode,
|
store_op: OpCode,
|
||||||
offset: u32,
|
offset: u32,
|
||||||
|
|
@ -276,8 +286,14 @@ fn test_store<'a>(
|
||||||
buf.append_u8(OpCode::END as u8);
|
buf.append_u8(OpCode::END as u8);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut inst = Instance::for_module(arena, module, DEFAULT_IMPORTS, is_debug_mode).unwrap();
|
let mut inst = Instance::for_module(
|
||||||
inst.call_export(module, start_fn_name, []).unwrap();
|
arena,
|
||||||
|
module,
|
||||||
|
DefaultImportDispatcher::default(),
|
||||||
|
is_debug_mode,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
inst.call_export(start_fn_name, []).unwrap();
|
||||||
|
|
||||||
inst.memory
|
inst.memory
|
||||||
}
|
}
|
||||||
|
|
@ -285,13 +301,13 @@ fn test_store<'a>(
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i32store() {
|
fn test_i32store() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I32STORE;
|
let store_op = OpCode::I32STORE;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I32(0x12345678);
|
let value = Value::I32(0x12345678);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]);
|
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]);
|
||||||
|
|
@ -300,13 +316,13 @@ fn test_i32store() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i64store() {
|
fn test_i64store() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I64STORE;
|
let store_op = OpCode::I64STORE;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I64(0x123456789abcdef0);
|
let value = Value::I64(0x123456789abcdef0);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -318,14 +334,14 @@ fn test_i64store() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_f32store() {
|
fn test_f32store() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::F32STORE;
|
let store_op = OpCode::F32STORE;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let inner: f32 = 1.23456;
|
let inner: f32 = 1.23456;
|
||||||
let value = Value::F32(inner);
|
let value = Value::F32(inner);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(&memory[index..][..4], &inner.to_le_bytes());
|
assert_eq!(&memory[index..][..4], &inner.to_le_bytes());
|
||||||
|
|
@ -334,14 +350,14 @@ fn test_f32store() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_f64store() {
|
fn test_f64store() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::F64STORE;
|
let store_op = OpCode::F64STORE;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let inner: f64 = 1.23456;
|
let inner: f64 = 1.23456;
|
||||||
let value = Value::F64(inner);
|
let value = Value::F64(inner);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(&memory[index..][..8], &inner.to_le_bytes());
|
assert_eq!(&memory[index..][..8], &inner.to_le_bytes());
|
||||||
|
|
@ -350,13 +366,13 @@ fn test_f64store() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i32store8() {
|
fn test_i32store8() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I32STORE8;
|
let store_op = OpCode::I32STORE8;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I32(0x12345678);
|
let value = Value::I32(0x12345678);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]);
|
assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]);
|
||||||
|
|
@ -365,13 +381,13 @@ fn test_i32store8() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i32store16() {
|
fn test_i32store16() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I32STORE16;
|
let store_op = OpCode::I32STORE16;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I32(0x12345678);
|
let value = Value::I32(0x12345678);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]);
|
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]);
|
||||||
|
|
@ -380,13 +396,13 @@ fn test_i32store16() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i64store8() {
|
fn test_i64store8() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I64STORE8;
|
let store_op = OpCode::I64STORE8;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I64(0x123456789abcdef0);
|
let value = Value::I64(0x123456789abcdef0);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -398,13 +414,13 @@ fn test_i64store8() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i64store16() {
|
fn test_i64store16() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I64STORE16;
|
let store_op = OpCode::I64STORE16;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I64(0x123456789abcdef0);
|
let value = Value::I64(0x123456789abcdef0);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -416,13 +432,13 @@ fn test_i64store16() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_i64store32() {
|
fn test_i64store32() {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let mut module = WasmModule::new(&arena);
|
let module = arena.alloc(WasmModule::new(&arena));
|
||||||
|
|
||||||
let addr: u32 = 0x11;
|
let addr: u32 = 0x11;
|
||||||
let store_op = OpCode::I64STORE32;
|
let store_op = OpCode::I64STORE32;
|
||||||
let offset = 1;
|
let offset = 1;
|
||||||
let value = Value::I64(0x123456789abcdef0);
|
let value = Value::I64(0x123456789abcdef0);
|
||||||
let memory = test_store(&arena, &mut module, addr, store_op, offset, value);
|
let memory = test_store(&arena, module, addr, store_op, offset, value);
|
||||||
|
|
||||||
let index = (addr + offset) as usize;
|
let index = (addr + offset) as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,33 @@
|
||||||
|
use rand::prelude::*;
|
||||||
use roc_wasm_module::Value;
|
use roc_wasm_module::Value;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Read, StderrLock, StdoutLock, Write};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
pub const MODULE_NAME: &str = "wasi_snapshot_preview1";
|
pub const MODULE_NAME: &str = "wasi_snapshot_preview1";
|
||||||
|
|
||||||
pub struct WasiDispatcher<'a> {
|
pub struct WasiDispatcher<'a> {
|
||||||
pub args: &'a [&'a String],
|
pub args: &'a [&'a [u8]],
|
||||||
|
pub rng: ThreadRng,
|
||||||
|
pub files: Vec<WasiFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasiDispatcher<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
WasiDispatcher::new(&[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum WasiFile {
|
||||||
|
ReadOnly(Vec<u8>),
|
||||||
|
WriteOnly(Vec<u8>),
|
||||||
|
ReadWrite(Vec<u8>),
|
||||||
|
HostSystemFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WriteLock<'a> {
|
||||||
|
StdOut(StdoutLock<'a>),
|
||||||
|
Stderr(StderrLock<'a>),
|
||||||
|
RegularFile(&'a mut Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of WASI syscalls
|
/// Implementation of WASI syscalls
|
||||||
|
|
@ -13,8 +35,16 @@ pub struct WasiDispatcher<'a> {
|
||||||
/// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs
|
/// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs
|
||||||
/// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h
|
/// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h
|
||||||
impl<'a> WasiDispatcher<'a> {
|
impl<'a> WasiDispatcher<'a> {
|
||||||
pub fn new(args: &'a [&'a String]) -> Self {
|
pub fn new(args: &'a [&'a [u8]]) -> Self {
|
||||||
WasiDispatcher { args }
|
WasiDispatcher {
|
||||||
|
args,
|
||||||
|
rng: thread_rng(),
|
||||||
|
files: vec![
|
||||||
|
WasiFile::HostSystemFile,
|
||||||
|
WasiFile::HostSystemFile,
|
||||||
|
WasiFile::HostSystemFile,
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(
|
pub fn dispatch(
|
||||||
|
|
@ -35,7 +65,7 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
for arg in self.args {
|
for arg in self.args {
|
||||||
write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32);
|
write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32);
|
||||||
let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()];
|
let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()];
|
||||||
bytes_target.copy_from_slice(arg.as_bytes());
|
bytes_target.copy_from_slice(arg);
|
||||||
memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination
|
memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination
|
||||||
ptr_argv_buf += arg.len() + 1;
|
ptr_argv_buf += arg.len() + 1;
|
||||||
ptr_ptr_argv += 4;
|
ptr_ptr_argv += 4;
|
||||||
|
|
@ -61,8 +91,8 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
}
|
}
|
||||||
"environ_get" => todo!("WASI {}({:?})", function_name, arguments),
|
"environ_get" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments),
|
"environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"clock_res_get" => success_code, // this dummy implementation seems to be good enough
|
"clock_res_get" => success_code, // this dummy implementation seems to be good enough for some functions
|
||||||
"clock_time_get" => success_code, // this dummy implementation seems to be good enough
|
"clock_time_get" => success_code,
|
||||||
"fd_advise" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_advise" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_allocate" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_allocate" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_close" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_close" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
|
|
@ -74,18 +104,90 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
"fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_pread" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_pread" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_prestat_get" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_prestat_get" => {
|
||||||
"fd_prestat_dir_name" => todo!("WASI {}({:?})", function_name, arguments),
|
// The preopened file descriptor to query
|
||||||
|
let fd = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
// ptr_buf: Where the metadata will be written
|
||||||
|
// preopen type: 4 bytes, where 0=dir is the only one supported, it seems
|
||||||
|
// preopen name length: 4 bytes
|
||||||
|
let ptr_buf = arguments[1].expect_i32().unwrap() as usize;
|
||||||
|
memory[ptr_buf..][..8].copy_from_slice(&0u64.to_le_bytes());
|
||||||
|
if fd < self.files.len() {
|
||||||
|
success_code
|
||||||
|
} else {
|
||||||
|
println!("WASI warning: file descriptor {} does not exist", fd);
|
||||||
|
Some(Value::I32(Errno::Badf as i32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"fd_prestat_dir_name" => {
|
||||||
|
// We're not giving names to any of our files so just return success
|
||||||
|
success_code
|
||||||
|
}
|
||||||
"fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_read" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_read" => {
|
||||||
|
use WasiFile::*;
|
||||||
|
|
||||||
|
// file descriptor
|
||||||
|
let fd = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
// Array of IO vectors
|
||||||
|
let ptr_iovs = arguments[1].expect_i32().unwrap() as usize;
|
||||||
|
// Length of array
|
||||||
|
let iovs_len = arguments[2].expect_i32().unwrap();
|
||||||
|
// Out param: number of bytes read
|
||||||
|
let ptr_nread = arguments[3].expect_i32().unwrap() as usize;
|
||||||
|
|
||||||
|
// https://man7.org/linux/man-pages/man2/readv.2.html
|
||||||
|
// struct iovec {
|
||||||
|
// void *iov_base; /* Starting address */
|
||||||
|
// size_t iov_len; /* Number of bytes to transfer */
|
||||||
|
// };
|
||||||
|
|
||||||
|
let mut n_read: usize = 0;
|
||||||
|
match self.files.get(fd) {
|
||||||
|
Some(ReadOnly(content) | ReadWrite(content)) => {
|
||||||
|
for _ in 0..iovs_len {
|
||||||
|
let iov_base = read_u32(memory, ptr_iovs) as usize;
|
||||||
|
let iov_len = read_i32(memory, ptr_iovs + 4) as usize;
|
||||||
|
let remaining = content.len() - n_read;
|
||||||
|
let len = remaining.min(iov_len);
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
memory[iov_base..][..len].copy_from_slice(&content[n_read..][..len]);
|
||||||
|
n_read += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(HostSystemFile) if fd == 0 => {
|
||||||
|
let mut stdin = io::stdin();
|
||||||
|
for _ in 0..iovs_len {
|
||||||
|
let iov_base = read_u32(memory, ptr_iovs) as usize;
|
||||||
|
let iov_len = read_i32(memory, ptr_iovs + 4) as usize;
|
||||||
|
match stdin.read(&mut memory[iov_base..][..iov_len]) {
|
||||||
|
Ok(n) => {
|
||||||
|
n_read += n;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Some(Value::I32(Errno::Badf as i32)),
|
||||||
|
};
|
||||||
|
|
||||||
|
memory[ptr_nread..][..4].copy_from_slice(&(n_read as u32).to_le_bytes());
|
||||||
|
success_code
|
||||||
|
}
|
||||||
"fd_readdir" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_readdir" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_renumber" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_renumber" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_seek" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_seek" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_sync" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_sync" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_tell" => todo!("WASI {}({:?})", function_name, arguments),
|
"fd_tell" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"fd_write" => {
|
"fd_write" => {
|
||||||
|
use WasiFile::*;
|
||||||
|
|
||||||
// file descriptor
|
// file descriptor
|
||||||
let fd = arguments[0].expect_i32().unwrap();
|
let fd = arguments[0].expect_i32().unwrap() as usize;
|
||||||
// Array of IO vectors
|
// Array of IO vectors
|
||||||
let ptr_iovs = arguments[1].expect_i32().unwrap() as usize;
|
let ptr_iovs = arguments[1].expect_i32().unwrap() as usize;
|
||||||
// Length of array
|
// Length of array
|
||||||
|
|
@ -93,10 +195,18 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
// Out param: number of bytes written
|
// Out param: number of bytes written
|
||||||
let ptr_nwritten = arguments[3].expect_i32().unwrap() as usize;
|
let ptr_nwritten = arguments[3].expect_i32().unwrap() as usize;
|
||||||
|
|
||||||
let mut write_lock = match fd {
|
// Grab a lock for stdout/stderr before the loop rather than re-acquiring over and over.
|
||||||
1 => Ok(io::stdout().lock()),
|
// Not really necessary for other files, but it's easier to use the same structure.
|
||||||
2 => Err(io::stderr().lock()),
|
let mut write_lock = match self.files.get_mut(fd) {
|
||||||
_ => return Some(Value::I32(Errno::Inval as i32)),
|
Some(HostSystemFile) => match fd {
|
||||||
|
1 => WriteLock::StdOut(io::stdout().lock()),
|
||||||
|
2 => WriteLock::Stderr(io::stderr().lock()),
|
||||||
|
_ => return Some(Value::I32(Errno::Inval as i32)),
|
||||||
|
},
|
||||||
|
Some(WriteOnly(content) | ReadWrite(content)) => {
|
||||||
|
WriteLock::RegularFile(content)
|
||||||
|
}
|
||||||
|
_ => return Some(Value::I32(Errno::Badf as i32)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut n_written: i32 = 0;
|
let mut n_written: i32 = 0;
|
||||||
|
|
@ -119,12 +229,16 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
let bytes = &memory[iov_base..][..iov_len as usize];
|
let bytes = &memory[iov_base..][..iov_len as usize];
|
||||||
|
|
||||||
match &mut write_lock {
|
match &mut write_lock {
|
||||||
Ok(stdout) => {
|
WriteLock::StdOut(stdout) => {
|
||||||
n_written += stdout.write(bytes).unwrap() as i32;
|
n_written += stdout.write(bytes).unwrap() as i32;
|
||||||
}
|
}
|
||||||
Err(stderr) => {
|
WriteLock::Stderr(stderr) => {
|
||||||
n_written += stderr.write(bytes).unwrap() as i32;
|
n_written += stderr.write(bytes).unwrap() as i32;
|
||||||
}
|
}
|
||||||
|
WriteLock::RegularFile(content) => {
|
||||||
|
content.extend_from_slice(bytes);
|
||||||
|
n_written += bytes.len() as i32;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +270,16 @@ impl<'a> WasiDispatcher<'a> {
|
||||||
}
|
}
|
||||||
"proc_raise" => todo!("WASI {}({:?})", function_name, arguments),
|
"proc_raise" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"sched_yield" => todo!("WASI {}({:?})", function_name, arguments),
|
"sched_yield" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"random_get" => todo!("WASI {}({:?})", function_name, arguments),
|
"random_get" => {
|
||||||
|
// A pointer to a buffer where the random bytes will be written
|
||||||
|
let ptr_buf = arguments[0].expect_i32().unwrap() as usize;
|
||||||
|
// The number of bytes that will be written
|
||||||
|
let buf_len = arguments[1].expect_i32().unwrap() as usize;
|
||||||
|
for i in 0..buf_len {
|
||||||
|
memory[ptr_buf + i] = self.rng.gen();
|
||||||
|
}
|
||||||
|
success_code
|
||||||
|
}
|
||||||
"sock_recv" => todo!("WASI {}({:?})", function_name, arguments),
|
"sock_recv" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"sock_send" => todo!("WASI {}({:?})", function_name, arguments),
|
"sock_send" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
"sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments),
|
"sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue