use super::{RefCount, RefCountLoc}; use crate::helpers::from_wasm32_memory::FromWasm32Memory; use bumpalo::Bump; use roc_collections::all::MutSet; use roc_gen_wasm::wasm32_result::Wasm32Result; use roc_gen_wasm::DEBUG_SETTINGS; use roc_load::{ExecutionMode, LoadConfig, Threading}; use roc_packaging::cache::RocCacheDir; use roc_reporting::report::DEFAULT_PALETTE_HTML; use roc_solve::FunctionKind; use roc_std::RocStr; use roc_wasm_interp::{wasi, ImportDispatcher, Instance, WasiDispatcher}; use roc_wasm_module::{Export, ExportType, Value, WasmModule}; use std::marker::PhantomData; use std::path::PathBuf; const TEST_WRAPPER_NAME: &str = "test_wrapper"; const INIT_REFCOUNT_NAME: &str = "init_refcount_test"; macro_rules! host_bytes_path { () => { // Should manually match build.rs. include_bytes! requires a string literal. concat!(env!("OUT_DIR"), "/wasm_test_platform.wasm") }; } fn promote_expr_to_module(src: &str) -> String { let mut buffer = String::from("app \"test\" provides [main] to \"./platform\"\n\nmain =\n"); for line in src.lines() { // indent the body! buffer.push_str(" "); buffer.push_str(line); buffer.push('\n'); } buffer } fn write_final_wasm() -> bool { use roc_debug_flags::dbg_do; dbg_do!(roc_debug_flags::ROC_WRITE_FINAL_WASM, { return true; }); DEBUG_SETTINGS.keep_test_binary } #[allow(dead_code)] pub fn compile_to_wasm_bytes<'a, T: Wasm32Result>( arena: &'a bumpalo::Bump, src: &str, test_wrapper_type_info: PhantomData, ) -> Vec { let platform_bytes = include_bytes!(host_bytes_path!()); println!("Loading test host {}", host_bytes_path!()); let compiled_bytes = compile_roc_to_wasm_bytes(arena, platform_bytes, src, test_wrapper_type_info); if write_final_wasm() { let build_dir_hash = crate::helpers::src_hash(src); crate::helpers::save_wasm_file(&compiled_bytes, build_dir_hash) }; compiled_bytes } fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>( arena: &'a bumpalo::Bump, host_bytes: &[u8], src: &str, _test_wrapper_type_info: PhantomData, ) -> Vec { let filename = PathBuf::from("Test.roc"); let src_dir = PathBuf::from("fake/test/path"); let module_src; let temp; if src.starts_with("app") { // this is already a module module_src = src; } else { // this is an expression, promote it to a module temp = promote_expr_to_module(src); module_src = &temp; } let load_config = LoadConfig { target: roc_target::Target::Wasm32, render: roc_reporting::report::RenderTarget::ColorTerminal, palette: DEFAULT_PALETTE_HTML, threading: Threading::Single, exec_mode: ExecutionMode::Executable, function_kind: FunctionKind::LambdaSet, }; let loaded = roc_load::load_and_monomorphize_from_str( arena, filename, module_src, src_dir, None, RocCacheDir::Disallowed, load_config, ); let loaded = loaded.expect("failed to load module"); use roc_load::MonomorphizedModule; let MonomorphizedModule { module_id, procedures, mut interns, exposed_to_host, mut layout_interner, .. } = loaded; debug_assert_eq!(exposed_to_host.top_level_values.len(), 1); let exposed_to_host = exposed_to_host .top_level_values .keys() .copied() .collect::>(); let env = roc_gen_wasm::Env { arena, module_id, exposed_to_host, stack_bytes: roc_gen_wasm::Env::DEFAULT_STACK_BYTES, }; let host_module = roc_gen_wasm::parse_host(env.arena, host_bytes).unwrap_or_else(|e| { panic!( "I ran into a problem with the host object file, {} at offset 0x{:x}:\n{}", host_bytes_path!(), e.offset, e.message ) }); let (mut module, mut called_fns, main_fn_index) = roc_gen_wasm::build_app_module( &env, &mut layout_interner, &mut interns, host_module, procedures, ); T::insert_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index); called_fns.push(true); // Export the initialiser function for refcount tests let init_refcount_idx = module .names .function_names .iter() .filter(|(_, name)| *name == INIT_REFCOUNT_NAME) .map(|(i, _)| *i) .next() .unwrap(); module.export.append(Export { name: INIT_REFCOUNT_NAME, ty: ExportType::Func, index: init_refcount_idx, }); module.eliminate_dead_code(env.arena, called_fns); let mut app_module_bytes = std::vec::Vec::with_capacity(module.size()); module.serialize(&mut app_module_bytes); app_module_bytes } #[allow(dead_code)] pub fn assert_evals_to_help(src: &str, phantom: PhantomData) -> Result where T: FromWasm32Memory + Wasm32Result, { let arena = bumpalo::Bump::new(); let wasm_bytes = crate::helpers::wasm::compile_to_wasm_bytes(&arena, src, phantom); run_wasm_test_bytes::(TEST_WRAPPER_NAME, wasm_bytes) } struct TestDispatcher<'a> { wasi: WasiDispatcher<'a>, } impl<'a> ImportDispatcher for TestDispatcher<'a> { fn dispatch( &mut self, module_name: &str, function_name: &str, arguments: &[Value], memory: &mut [u8], ) -> Option { if module_name == wasi::MODULE_NAME { self.wasi.dispatch(function_name, arguments, memory) } else if module_name == "env" && function_name == "send_panic_msg_to_rust" { let msg_ptr = arguments[0].expect_i32().unwrap(); let panic_tag = arguments[1].expect_i32().unwrap(); let roc_msg = RocStr::decode(memory, msg_ptr as _); let msg = match panic_tag { 0 => format!(r#"Roc failed with message: "{roc_msg}""#), 1 => format!(r#"User crash with message: "{roc_msg}""#), _ => format!(r#"Got an invald panic tag: "{panic_tag}""#), }; panic!("{}", msg) } else { panic!( "TestDispatcher does not implement {}.{}", module_name, function_name ); } } } pub(crate) fn run_wasm_test_bytes( test_wrapper_name: &str, wasm_bytes: Vec, ) -> Result where T: FromWasm32Memory + Wasm32Result, { let arena = Bump::new(); let require_relocatable = false; let module = WasmModule::preload(&arena, &wasm_bytes, require_relocatable) .map_err(|e| format!("{:?}", e))?; run_wasm_test_module(&arena, test_wrapper_name, &module) } pub(crate) fn run_wasm_test_module<'a, T>( arena: &'a Bump, test_wrapper_name: &str, module: &WasmModule<'a>, ) -> Result where T: FromWasm32Memory + Wasm32Result, { let dispatcher = TestDispatcher { wasi: wasi::WasiDispatcher::default(), }; 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 opt_value = inst.call_export(test_wrapper_name, [])?; 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 output = ::decode(&inst.memory, addr as u32); Ok(output) } #[allow(dead_code)] pub fn assert_wasm_refcounts_help( src: &str, phantom: PhantomData, refcount_locs: &[RefCountLoc], ) -> Result, String> where T: FromWasm32Memory + Wasm32Result, { let arena = bumpalo::Bump::new(); let wasm_bytes = crate::helpers::wasm::compile_to_wasm_bytes(&arena, src, phantom); let require_relocatable = false; let module = WasmModule::preload(&arena, &wasm_bytes, require_relocatable) .map_err(|e| format!("{:?}", e))?; let dispatcher = TestDispatcher { wasi: wasi::WasiDispatcher::default(), }; 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)?; // Allocate a vector in the test host that refcounts will be copied into let num_refcounts = refcount_locs.len(); let mut refcount_vector_addr: i32 = inst .call_export(INIT_REFCOUNT_NAME, [Value::I32(num_refcounts as i32)])? .ok_or_else(|| format!("No return address from {}", INIT_REFCOUNT_NAME))? .expect_i32() .map_err(|type_err| format!("{:?}", type_err))?; // Run the test, ignoring the result let _result_addr: i32 = inst .call_export(TEST_WRAPPER_NAME, [])? .ok_or_else(|| format!("No return address from {}", TEST_WRAPPER_NAME))? .expect_i32() .map_err(|type_err| format!("{:?}", type_err))?; // Read the length of the vector in the C host let actual_num_refcounts = read_i32(&inst.memory, refcount_vector_addr) as usize; if actual_num_refcounts != num_refcounts { return Err(format!( "Expected {} refcounts but got {}", num_refcounts, actual_num_refcounts )); } // Read the refcounts let mut refcounts = Vec::with_capacity(num_refcounts); for refcount_loc in refcount_locs { // Get the next RC pointer from the host's vector refcount_vector_addr += 4; let mut rc_ptr = read_i32(&inst.memory, refcount_vector_addr); let rc = if rc_ptr == 0 { RefCount::Deallocated } else { // If size is store on the heap for this type, the rc pointer is directly after. if matches!(refcount_loc, RefCountLoc::AfterSize) { rc_ptr += 4 } // Dereference the RC pointer and decode its value from the negative number format let rc_encoded = read_i32(&inst.memory, rc_ptr); if rc_encoded == 0 { RefCount::Constant } else { let rc = rc_encoded - i32::MIN + 1; RefCount::Live(rc as u32) } }; refcounts.push(rc); } Ok(refcounts) } fn read_i32(memory: &[u8], addr: i32) -> i32 { let index = addr as usize; let mut bytes = [0; 4]; bytes.copy_from_slice(&memory[index..][..4]); i32::from_le_bytes(bytes) } /// Print out hex bytes of the test result, and a few words on either side /// Can be handy for debugging misalignment issues etc. #[allow(dead_code)] pub fn debug_memory_hex(memory_bytes: &[u8], address: i32, size: usize) { let memory_words: &[u32] = unsafe { std::slice::from_raw_parts(memory_bytes.as_ptr().cast(), memory_bytes.len() / 4) }; let extra_words = 2; let result_start = (address as usize) / 4; let result_end = result_start + ((size + 3) / 4); let start = result_start - extra_words; let end = result_end + extra_words; for index in start..end { let result_marker = if index >= result_start && index < result_end { "|" } else { " " }; println!( "{:x} {} {:08x}", index * 4, result_marker, memory_words[index] ); } println!(); } #[allow(unused_macros)] macro_rules! assert_evals_to { ($src:expr, $expected:expr, $ty:ty) => { $crate::helpers::wasm::assert_evals_to!( $src, $expected, $ty, $crate::helpers::wasm::identity, false ) }; ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { $crate::helpers::wasm::assert_evals_to!($src, $expected, $ty, $transform, false) }; ($src:expr, $expected:expr, $ty:ty, $transform:expr, $ignore_problems: expr) => {{ let phantom = std::marker::PhantomData; let _ = $ignore_problems; // Always ignore "problems"! One backend (LLVM) is enough to cover them. match $crate::helpers::wasm::assert_evals_to_help::<$ty>($src, phantom) { Err(msg) => panic!("{}", msg), Ok(actual) => { assert_eq!($transform(actual), $expected) } } }}; } #[allow(dead_code)] pub fn identity(value: T) -> T { value } #[allow(unused_macros)] macro_rules! assert_refcounts { // We need the result type to generate the test_wrapper, even though we ignore the value! // We can't just call `main` with no args, because some tests return structs, via pointer arg! // Also we need to know how much stack space to reserve for the struct. ($src: expr, $ty: ty, $expected: expr) => {{ let phantom = std::marker::PhantomData; let (refcount_locs, expected_refcounts): ( Vec<$crate::helpers::RefCountLoc>, Vec<$crate::helpers::RefCount>, ) = $expected.into_iter().map(|x| *x).unzip(); let result = $crate::helpers::wasm::assert_wasm_refcounts_help::<$ty>($src, phantom, &refcount_locs); match result { Err(msg) => panic!("{:?}", msg), Ok(actual_refcounts) => { assert_eq!(actual_refcounts, expected_refcounts) } } }}; } #[allow(unused_imports)] pub(crate) use assert_evals_to; #[allow(unused_imports)] pub(crate) use assert_refcounts;