repl_test: start re-writing the Wasm tests to use roc_wasm_interp

This commit is contained in:
Brian Carroll 2022-12-14 22:44:09 +00:00
parent e6325fa78f
commit eaa3f14fb0
No known key found for this signature in database
GPG key ID: 5C7B2EC4101703C0
7 changed files with 204 additions and 3 deletions

3
Cargo.lock generated
View file

@ -3494,12 +3494,15 @@ checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157"
name = "repl_test" name = "repl_test"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"bumpalo",
"indoc", "indoc",
"lazy_static", "lazy_static",
"roc_build", "roc_build",
"roc_cli", "roc_cli",
"roc_repl_cli", "roc_repl_cli",
"roc_test_utils", "roc_test_utils",
"roc_wasm_interp",
"roc_wasm_module",
"strip-ansi-escapes", "strip-ansi-escapes",
"wasmer", "wasmer",
"wasmer-wasi", "wasmer-wasi",

View file

@ -10,16 +10,19 @@ description = "Tests the roc REPL."
roc_cli = {path = "../cli"} roc_cli = {path = "../cli"}
[dependencies] [dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0" # TODO: delete this, it was only for wasmer
[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" 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"}
roc_wasm_module = {path = "../wasm_module"} # todo: would be nicer not to need this
# Wasmer singlepass compiler only works on x86_64. # Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dev-dependencies] [target.'cfg(target_arch = "x86_64")'.dev-dependencies]

24
crates/repl_test/build.rs Normal file
View file

@ -0,0 +1,24 @@
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
if std::env::var("CARGO_FEATURE_WASM").is_ok() {
Command::new("cargo")
.args([
"build",
"--target",
"wasm32-wasi",
"-p",
"roc_repl_wasm",
"--no-default-features",
"--features",
"wasi_test",
"--release",
"--target-dir",
"../../.ignore/repl_test_target",
])
.spawn()
.unwrap();
}
}

View file

@ -14,3 +14,6 @@ mod cli;
#[cfg(all(test, feature = "wasm"))] #[cfg(all(test, feature = "wasm"))]
mod wasm; mod wasm;
#[cfg(all(test, feature = "wasm"))]
mod wasm_interp;

View file

@ -8,7 +8,7 @@ use crate::cli::{expect_failure, expect_success, repl_eval};
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::wasm::{expect_failure, expect_success}; use crate::wasm_interp::{expect_failure, expect_success};
#[test] #[test]
fn literal_0() { fn literal_0() {

View file

@ -0,0 +1,168 @@
use bumpalo::Bump;
use roc_wasm_interp::{
wasi, DefaultImportDispatcher, ImportDispatcher, Instance, WasiDispatcher, DEFAULT_IMPORTS,
};
use roc_wasm_module::{Value, WasmModule};
const COMPILER_BYTES: &[u8] =
include_bytes!("../../../.ignore/repl_test_target/wasm32-wasi/release/roc_repl_wasm.wasm");
struct CompilerDispatcher<'a> {
arena: &'a Bump,
src: &'a str,
answer: String,
wasi: WasiDispatcher<'a>,
app: Option<(WasmModule<'a>, Instance<'a, DefaultImportDispatcher<'a>>)>,
result_addr: Option<i32>,
}
impl<'a> ImportDispatcher for CompilerDispatcher<'a> {
fn dispatch(
&mut self,
module_name: &str,
function_name: &str,
arguments: &[Value],
compiler_memory: &mut [u8],
) -> Option<Value> {
let unknown = || {
panic!(
"TestDispatcher does not implement {}.{}",
module_name, function_name
)
};
if module_name == wasi::MODULE_NAME {
self.wasi
.dispatch(function_name, arguments, compiler_memory)
} else if module_name == "env" {
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];
let require_reloc = false;
let module = WasmModule::preload(self.arena, app_bytes, require_reloc).unwrap();
let is_debug_mode = false;
let instance =
Instance::for_module(self.arena, &module, DEFAULT_IMPORTS, is_debug_mode)
.unwrap();
self.app = Some((module, instance));
let ok = Value::I32(true as i32);
Some(ok)
}
"test_run_app" => {
// fn test_run_app() -> usize;
assert_eq!(arguments.len(), 0);
match &mut self.app {
Some((module, instance)) => {
let result_addr = instance
.call_export(module, "wrapper", [])
.unwrap()
.expect("No return address from wrapper")
.expect_i32()
.unwrap();
self.result_addr = Some(result_addr);
let memory_size = instance.memory.len();
Some(Value::I32(memory_size as i32))
}
None => panic!("Trying to run the app but it hasn't been created"),
}
}
"test_get_result_and_memory" => {
// Copy the app's entire memory buffer into the compiler's memory,
// and return the location in that buffer where we can find the app result.
// fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
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 {
unknown()
}
}
}
fn run(src: &'static str) -> Result<String, String> {
let arena = Bump::new();
let require_reloc = false;
let module = WasmModule::preload(&arena, COMPILER_BYTES, require_reloc).unwrap();
let mut instance = {
let dispatcher = CompilerDispatcher {
arena: &arena,
src,
answer: String::new(),
wasi: WasiDispatcher { args: &[] },
app: None,
result_addr: None,
};
let is_debug_mode = false;
Instance::for_module(&arena, &module, dispatcher, is_debug_mode).unwrap()
};
let wasm_ok: i32 = instance
.call_export(&module, "entrypoint_from_test", [])
.unwrap()
.unwrap()
.expect_i32()
.unwrap();
let answer_str = instance.import_dispatcher.answer.to_owned();
if wasm_ok == 0 {
Err(answer_str)
} else {
Ok(answer_str)
}
}
#[allow(dead_code)]
pub fn expect_success(input: &'static str, expected: &str) {
assert_eq!(run(input), Ok(expected.into()));
}
#[allow(dead_code)]
pub fn expect_failure(input: &'static str, expected: &str) {
assert_eq!(run(input), Err(expected.into()));
}

View file

@ -50,7 +50,7 @@ pub struct Instance<'a, I: ImportDispatcher> {
/// Cache for branching instructions /// Cache for branching instructions
branch_cache: Vec<'a, BranchCacheEntry>, branch_cache: Vec<'a, BranchCacheEntry>,
/// 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