Merge remote-tracking branch 'origin/trunk' into expect-dont-panic

This commit is contained in:
Richard Feldman 2022-01-30 20:22:26 -05:00
commit a55ff62e6c
No known key found for this signature in database
GPG key ID: 7E4127D1E4241798
501 changed files with 26570 additions and 12276 deletions

View file

@ -0,0 +1,369 @@
<html>
<head>
<style>
body {
background-color: #111;
color: #00c000;
font-family: sans-serif;
font-size: 18px;
}
section {
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
}
h1 {
margin: 32px auto;
}
li {
margin: 8px;
}
code {
color: #0080ff;
background-color: #000;
padding: 1px 4px;
font-size: 16px;
border-radius: 6px;
}
input,
button {
font-size: 20px;
font-weight: bold;
padding: 4px 12px;
}
small {
font-style: italic;
}
#error {
color: #f00;
}
.controls {
margin-top: 64px;
display: flex;
flex-direction: column;
}
.controls button {
margin: 16px;
}
.row {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 24px;
}
.row-file label {
margin: 0 32px;
}
</style>
</head>
<body>
<section>
<h1>Debug Wasm tests in the browser!</h1>
<p>
You can step through the generated code instruction-by-instruction, and
examine memory contents
</p>
<h3>Steps</h3>
<ul>
<li>
In <code>gen_wasm/src/lib.rs</code>, set
<code>DEBUG_LOG_SETTINGS.keep_test_binary = true</code>
</li>
<li>Run <code>cargo test-gen-wasm -- my_test --nocapture</code></li>
<li>
Look for the path written to the console for
<code>final.wasm</code> and select it in the file picker below
</li>
<li>
Open the browser DevTools <br />
<small> Control+Shift+I or Command+Option+I or F12 </small>
</li>
<li>
Click one of the buttons below, depending on what kind of test it is.
<br />
<small>
Only one of them will work. The other will probably crash or
something.
</small>
</li>
<li>
The debugger should pause just before entering the first Wasm call.
Step into a couple of Wasm calls until you reach your test code in
<code>$#UserApp_main_1</code>
</li>
<li>
Chrome DevTools now has a Memory Inspector panel! In the debugger,
find <code>Module -> memories -> $memory</code>. Right click and
select "Reveal in Memory Inspector"
</li>
</ul>
<div class="controls">
<div class="row row-file">
<label for="wasm-file">Select final.wasm</label>
<input id="wasm-file" type="file" />
</div>
<div id="error" class="row"></div>
<div class="row">
<button id="button-expression">Run as Roc expression test</button>
<button id="button-refcount">Run as reference counting test</button>
</div>
</div>
</section>
<script>
const file_input = document.getElementById("wasm-file");
const button_expression = document.getElementById("button-expression");
const button_refcount = document.getElementById("button-refcount");
const error_box = document.getElementById("error");
button_expression.onclick = runExpressionTest;
button_refcount.onclick = runRefcountTest;
file_input.onchange = function () {
error_box.innerHTML = "";
};
async function runExpressionTest() {
const file = getFile();
const instance = await compileFileToInstance(file);
debugger; // Next call is Wasm! Step into test_wrapper, then $#UserApp_main_1
instance.exports.test_wrapper();
}
async function runRefcountTest() {
const file = getFile();
const instance = await compileFileToInstance(file);
const MAX_ALLOCATIONS = 100;
const refcount_vector_addr =
instance.exports.init_refcount_test(MAX_ALLOCATIONS);
debugger; // Next call is Wasm! Step into test_wrapper, then $#UserApp_main_1
instance.exports.test_wrapper();
const words = new Uint32Array(instance.exports.memory.buffer);
function deref(addr8) {
return words[addr8 >> 2];
}
const actual_len = deref(refcount_vector_addr);
const rc_pointers = [];
for (let i = 0; i < actual_len; i++) {
const offset = (1 + i) << 2;
const rc_ptr = deref(refcount_vector_addr + offset);
rc_pointers.push(rc_ptr);
}
const rc_encoded = rc_pointers.map((ptr) => ptr && deref(ptr));
const rc_encoded_hex = rc_encoded.map((x) =>
x ? x.toString(16) : "(deallocated)"
);
const rc_values = rc_encoded.map((x) => x && x - 0x80000000 + 1);
console.log({ rc_values, rc_encoded_hex });
}
function getFile() {
const { files } = file_input;
if (!files.length) {
const msg = "Select a file!";
error_box.innerHTML = msg;
throw new Error(msg);
}
return files[0];
}
async function compileFileToInstance(file) {
const buffer = await file.arrayBuffer();
const wasiLinkObject = {};
const importObject = createFakeWasiImports(wasiLinkObject);
const result = await WebAssembly.instantiate(buffer, importObject);
wasiLinkObject.memory8 = new Uint8Array(
result.instance.exports.memory.buffer
);
wasiLinkObject.memory32 = new Uint32Array(
result.instance.exports.memory.buffer
);
return result.instance;
}
// If you print to stdout (for example in the platform), it calls these WASI imports.
// This implementation uses console.log
function createFakeWasiImports(wasiLinkObject) {
const decoder = new TextDecoder();
// fd_close : (i32) -> i32
// Close a file descriptor. Note: This is similar to close in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_close.html
function fd_close(fd) {
console.warn(`fd_close: ${{ fd }}`);
return 0; // error code
}
// fd_fdstat_get : (i32, i32) -> i32
// Get the attributes of a file descriptor.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_fdstat_get.html
function fd_fdstat_get(fd, stat_mut_ptr) {
/*
Tell WASI that stdout is a tty (no seek or tell)
https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/isatty.c
*Not* a tty if:
(statbuf.fs_filetype != __WASI_FILETYPE_CHARACTER_DEVICE ||
(statbuf.fs_rights_base & (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL)) != 0)
So it's sufficient to set:
.fs_filetype = __WASI_FILETYPE_CHARACTER_DEVICE
.fs_rights_base = 0
https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/headers/public/wasi/api.h
typedef uint8_t __wasi_filetype_t;
typedef uint16_t __wasi_fdflags_t;
typedef uint64_t __wasi_rights_t;
#define __WASI_FILETYPE_CHARACTER_DEVICE (UINT8_C(2))
typedef struct __wasi_fdstat_t { // 24 bytes total
__wasi_filetype_t fs_filetype; // 1 byte
// 1 byte padding
__wasi_fdflags_t fs_flags; // 2 bytes
// 4 bytes padding
__wasi_rights_t fs_rights_base; // 8 bytes
__wasi_rights_t fs_rights_inheriting; // 8 bytes
} __wasi_fdstat_t;
*/
// console.warn(`fd_fdstat_get: ${{ fd, stat_mut_ptr }}`);
const WASI_FILETYPE_CHARACTER_DEVICE = 2;
wasiLinkObject.memory8[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE;
wasiLinkObject.memory8
.slice(stat_mut_ptr + 1, stat_mut_ptr + 24)
.fill(0);
return 0; // error code
}
// fd_seek : (i32, i64, i32, i32) -> i32
// Move the offset of a file descriptor. Note: This is similar to lseek in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_seek.html
function fd_seek(fd, offset, whence, newoffset_mut_ptr) {
console.warn(`fd_seek: ${{ fd, offset, whence, newoffset_mut_ptr }}`);
return 0;
}
// fd_write : (i32, i32, i32, i32) -> i32
// Write to a file descriptor. Note: This is similar to `writev` in POSIX.
// https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_write.html
function fd_write(fd, iovs_ptr, iovs_len, nwritten_mut_ptr) {
let string_buffer = "";
let nwritten = 0;
for (let i = 0; i < iovs_len; i++) {
const index32 = iovs_ptr >> 2;
const base = wasiLinkObject.memory32[index32];
const len = wasiLinkObject.memory32[index32 + 1];
iovs_ptr += 8;
if (!len) continue;
nwritten += len;
// For some reason we often get negative-looking buffer lengths with junk data.
// Just skip the console.log, but still increase nwritten or it will loop forever.
// Dunno why this happens, but it's working fine for printf debugging ¯\_(ツ)_/¯
if (len >> 31) {
break;
}
const buf = wasiLinkObject.memory8.slice(base, base + len);
const chunk = decoder.decode(buf);
string_buffer += chunk;
}
wasiLinkObject.memory32[nwritten_mut_ptr >> 2] = nwritten;
if (string_buffer) {
console.log(string_buffer);
}
return 0;
}
// proc_exit : (i32) -> nil
function proc_exit(exit_code) {
if (exit_code) {
throw new Error(`Wasm exited with code ${exit_code}`);
}
}
// Signatures from wasm_test_platform.o
const sig2 = (i32) => {};
const sig6 = (i32a, i32b) => 0;
const sig7 = (i32a, i32b, i32c) => 0;
const sig9 = (i32a, i64b, i32c) => 0;
const sig10 = (i32a, i64b, i64c, i32d) => 0;
const sig11 = (i32a, i64b, i64c) => 0;
const sig12 = (i32a) => 0;
const sig13 = (i32a, i64b) => 0;
const sig14 = (i32a, i32b, i32c, i64d, i32e) => 0;
const sig15 = (i32a, i32b, i32c, i32d) => 0;
const sig16 = (i32a, i64b, i32c, i32d) => 0;
const sig17 = (i32a, i32b, i32c, i32d, i32e) => 0;
const sig18 = (i32a, i32b, i32c, i32d, i64e, i64f, i32g) => 0;
const sig19 = (i32a, i32b, i32c, i32d, i32e, i32f, i32g) => 0;
const sig20 = (i32a, i32b, i32c, i32d, i32e, i64f, i64g, i32h, i32i) =>
0;
const sig21 = (i32a, i32b, i32c, i32d, i32e, i32f) => 0;
const sig22 = () => 0;
return {
wasi_snapshot_preview1: {
args_get: sig6,
args_sizes_get: sig6,
environ_get: sig6,
environ_sizes_get: sig6,
clock_res_get: sig6,
clock_time_get: sig9,
fd_advise: sig10,
fd_allocate: sig11,
fd_close,
fd_datasync: sig12,
fd_fdstat_get,
fd_fdstat_set_flags: sig6,
fd_fdstat_set_rights: sig11,
fd_filestat_get: sig6,
fd_filestat_set_size: sig13,
fd_filestat_set_times: sig10,
fd_pread: sig14,
fd_prestat_get: sig6,
fd_prestat_dir_name: sig7,
fd_pwrite: sig14,
fd_read: sig15,
fd_readdir: sig14,
fd_renumber: sig6,
fd_seek,
fd_sync: sig12,
fd_tell: sig6,
fd_write,
path_create_directory: sig7,
path_filestat_get: sig17,
path_filestat_set_times: sig18,
path_link: sig19,
path_open: sig20,
path_readlink: sig21,
path_remove_directory: sig7,
path_rename: sig21,
path_symlink: sig17,
path_unlink_file: sig7,
poll_oneoff: sig15,
proc_exit,
proc_raise: sig12,
sched_yield: sig22,
random_get: sig6,
sock_recv: sig21,
sock_send: sig17,
sock_shutdown: sig6,
},
};
}
</script>
</body>
</html>

View file

@ -3,10 +3,11 @@ use roc_build::link::{link, LinkType};
use roc_builtins::bitcode;
use roc_can::builtins::builtin_defs_map;
use roc_collections::all::MutMap;
use roc_region::all::LineInfo;
use tempfile::tempdir;
#[allow(unused_imports)]
use roc_mono::ir::PRETTY_PRINT_IR_SYMBOLS;
use roc_mono::ir::pretty_print_ir_symbols;
#[allow(dead_code)]
fn promote_expr_to_module(src: &str) -> String {
@ -56,7 +57,7 @@ pub fn helper(
&stdlib,
src_dir,
exposed_types,
8,
roc_target::TargetInfo::default_x86_64(),
builtin_defs_map,
);
@ -75,7 +76,7 @@ pub fn helper(
// while you're working on the dev backend!
{
// println!("=========== Procedures ==========");
// if PRETTY_PRINT_IR_SYMBOLS {
// if pretty_print_ir_symbols() {
// println!("");
// for proc in procedures.values() {
// println!("{}", proc.to_pretty(200));
@ -94,7 +95,7 @@ pub fn helper(
// println!("=================================\n");
}
debug_assert_eq!(exposed_to_host.len(), 1);
debug_assert_eq!(exposed_to_host.values.len(), 1);
let main_fn_symbol = loaded.entry_point.symbol;
let main_fn_layout = loaded.entry_point.layout;
@ -124,6 +125,7 @@ pub fn helper(
continue;
}
let line_info = LineInfo::new(&src);
let src_lines: Vec<&str> = src.split('\n').collect();
let palette = DEFAULT_PALETTE;
@ -139,7 +141,7 @@ pub fn helper(
continue;
}
_ => {
let report = can_problem(&alloc, module_path.clone(), problem);
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -150,7 +152,7 @@ pub fn helper(
}
for problem in type_problems {
if let Some(report) = type_problem(&alloc, module_path.clone(), problem) {
if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) {
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -160,7 +162,7 @@ pub fn helper(
}
for problem in mono_problems {
let report = mono_problem(&alloc, module_path.clone(), problem);
let report = mono_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -177,7 +179,7 @@ pub fn helper(
let env = roc_gen_dev::Env {
arena,
module_id,
exposed_to_host: exposed_to_host.keys().copied().collect(),
exposed_to_host: exposed_to_host.values.keys().copied().collect(),
lazy_literals,
generate_allocators: true, // Needed for testing, since we don't have a platform
};

View file

@ -9,6 +9,7 @@ use roc_collections::all::{MutMap, MutSet};
use roc_gen_llvm::llvm::externs::add_default_roc_externs;
use roc_module::symbol::Symbol;
use roc_mono::ir::OptLevel;
use roc_region::all::LineInfo;
use roc_types::subs::VarStore;
use target_lexicon::Triple;
@ -28,8 +29,6 @@ pub fn test_builtin_defs(symbol: Symbol, var_store: &mut VarStore) -> Option<Def
builtin_defs_map(symbol, var_store)
}
// this is not actually dead code, but only used by cfg_test modules
// so "normally" it is dead, only at testing time is it used
#[allow(clippy::too_many_arguments)]
fn create_llvm_module<'a>(
arena: &'a bumpalo::Bump,
@ -43,6 +42,8 @@ fn create_llvm_module<'a>(
) -> (&'static str, String, &'a Module<'a>) {
use std::path::{Path, PathBuf};
let target_info = roc_target::TargetInfo::from(target);
let filename = PathBuf::from("Test.roc");
let src_dir = Path::new("fake/test/path");
@ -57,8 +58,6 @@ fn create_llvm_module<'a>(
module_src = &temp;
}
let ptr_bytes = target.pointer_width().unwrap().bytes() as u32;
let exposed_types = MutMap::default();
let loaded = roc_load::file::load_and_monomorphize_from_str(
arena,
@ -67,7 +66,7 @@ fn create_llvm_module<'a>(
stdlib,
src_dir,
exposed_types,
ptr_bytes,
target_info,
test_builtin_defs,
);
@ -107,6 +106,7 @@ fn create_llvm_module<'a>(
continue;
}
let line_info = LineInfo::new(&src);
let src_lines: Vec<&str> = src.split('\n').collect();
let palette = DEFAULT_PALETTE;
@ -123,7 +123,7 @@ fn create_llvm_module<'a>(
| RuntimeError(_)
| UnsupportedPattern(_, _)
| ExposedButNotDefined(_) => {
let report = can_problem(&alloc, module_path.clone(), problem);
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -132,7 +132,7 @@ fn create_llvm_module<'a>(
lines.push(buf);
}
_ => {
let report = can_problem(&alloc, module_path.clone(), problem);
let report = can_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -143,7 +143,7 @@ fn create_llvm_module<'a>(
}
for problem in type_problems {
if let Some(report) = type_problem(&alloc, module_path.clone(), problem) {
if let Some(report) = type_problem(&alloc, &line_info, module_path.clone(), problem) {
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -153,7 +153,7 @@ fn create_llvm_module<'a>(
}
for problem in mono_problems {
let report = mono_problem(&alloc, module_path.clone(), problem);
let report = mono_problem(&alloc, &line_info, module_path.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
@ -217,7 +217,7 @@ fn create_llvm_module<'a>(
context,
interns,
module,
ptr_bytes,
target_info,
is_gen_test,
// important! we don't want any procedures to get the C calling convention
exposed_to_host: MutSet::default(),
@ -375,7 +375,7 @@ pub fn helper_wasm<'a>(
use std::process::Command;
Command::new("zig")
Command::new(&crate::helpers::zig_executable())
.current_dir(dir_path)
.args(&[
"wasm-ld",
@ -593,10 +593,12 @@ macro_rules! assert_evals_to {
assert_evals_to!($src, $expected, $ty, $crate::helpers::llvm::identity);
}};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
// Same as above, except with an additional transformation argument.
// same as above, except with an additional transformation argument.
{
#[cfg(feature = "wasm-cli-run")]
$crate::helpers::llvm::assert_wasm_evals_to!($src, $expected, $ty, $transform, false);
$crate::helpers::llvm::assert_wasm_evals_to!(
$src, $expected, $ty, $transform, false, false
);
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
}
@ -641,6 +643,25 @@ macro_rules! assert_expect_failed {
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
};
macro_rules! expect_runtime_error_panic {
($src:expr) => {{
#[cfg(feature = "wasm-cli-run")]
$crate::helpers::llvm::assert_wasm_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::llvm::identity,
true // ignore problems
);
$crate::helpers::llvm::assert_llvm_evals_to!(
$src,
false, // fake value/type for eval
bool,
$crate::helpers::llvm::identity,
true // ignore problems
);
}};
}
#[allow(dead_code)]
@ -661,7 +682,7 @@ macro_rules! assert_non_opt_evals_to {
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
// Same as above, except with an additional transformation argument.
{
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform, false);
$crate::helpers::llvm::assert_llvm_evals_to!($src, $expected, $ty, $transform);
}
};
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {{
@ -679,3 +700,5 @@ pub(crate) use assert_llvm_evals_to;
pub(crate) use assert_non_opt_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_wasm_evals_to;
#[allow(unused_imports)]
pub(crate) use expect_runtime_error_panic;

View file

@ -10,6 +10,14 @@ pub mod wasm;
#[cfg(feature = "gen-wasm")]
pub mod wasm32_test_result;
#[allow(dead_code)]
pub fn zig_executable() -> String {
match std::env::var("ROC_ZIG") {
Ok(path) => path,
Err(_) => "zig".into(),
}
}
/// Used in the with_larger_debug_stack() function, for tests that otherwise
/// run out of stack space in debug builds (but don't in --release builds)
#[allow(dead_code)]

View file

@ -1,25 +1,23 @@
use core::cell::Cell;
use roc_gen_wasm::wasm_module::{Export, ExportType};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use tempfile::{tempdir, TempDir};
use wasmer::Memory;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use wasmer::{Memory, WasmPtr};
use crate::helpers::from_wasm32_memory::FromWasm32Memory;
use crate::helpers::wasm32_test_result::Wasm32TestResult;
use roc_builtins::bitcode;
use roc_can::builtins::builtin_defs_map;
use roc_collections::all::{MutMap, MutSet};
use roc_gen_wasm::MEMORY_NAME;
use roc_gen_wasm::{DEBUG_LOG_SETTINGS, MEMORY_NAME};
// Should manually match build.rs
const PLATFORM_FILENAME: &str = "wasm_test_platform";
const TEST_OUT_DIR: &str = env!("TEST_GEN_OUT");
const LIBC_A_FILE: &str = env!("TEST_GEN_WASM_LIBC_PATH");
#[allow(unused_imports)]
use roc_mono::ir::PRETTY_PRINT_IR_SYMBOLS;
const OUT_DIR_VAR: &str = "TEST_GEN_OUT";
const TEST_WRAPPER_NAME: &str = "test_wrapper";
const INIT_REFCOUNT_NAME: &str = "init_refcount_test";
fn promote_expr_to_module(src: &str) -> String {
let mut buffer = String::from("app \"test\" provides [ main ] to \"./platform\"\n\nmain =\n");
@ -34,15 +32,53 @@ fn promote_expr_to_module(src: &str) -> String {
buffer
}
pub enum TestType {
/// Test that some Roc code evaluates to the right result
Evaluate,
/// Test that some Roc values have the right refcount
Refcount,
}
#[allow(dead_code)]
pub fn helper_wasm<'a, T: Wasm32TestResult>(
pub fn compile_and_load<'a, T: Wasm32TestResult>(
arena: &'a bumpalo::Bump,
src: &str,
stdlib: &'a roc_builtins::std::StdLib,
_result_type_dummy: &T,
_test_wrapper_type_info: PhantomData<T>,
_test_type: TestType,
) -> wasmer::Instance {
use std::path::{Path, PathBuf};
let platform_bytes = load_platform_and_builtins();
let compiled_bytes =
compile_roc_to_wasm_bytes(arena, stdlib, &platform_bytes, src, _test_wrapper_type_info);
if DEBUG_LOG_SETTINGS.keep_test_binary {
let build_dir_hash = src_hash(src);
save_wasm_file(&compiled_bytes, build_dir_hash)
};
load_bytes_into_runtime(compiled_bytes)
}
fn load_platform_and_builtins() -> std::vec::Vec<u8> {
let out_dir = std::env::var(OUT_DIR_VAR).unwrap();
let platform_path = Path::new(&out_dir).join([PLATFORM_FILENAME, "o"].join("."));
std::fs::read(&platform_path).unwrap()
}
fn src_hash(src: &str) -> u64 {
let mut hash_state = DefaultHasher::new();
src.hash(&mut hash_state);
hash_state.finish()
}
fn compile_roc_to_wasm_bytes<'a, T: Wasm32TestResult>(
arena: &'a bumpalo::Bump,
stdlib: &'a roc_builtins::std::StdLib,
preload_bytes: &[u8],
src: &str,
_test_wrapper_type_info: PhantomData<T>,
) -> Vec<u8> {
let filename = PathBuf::from("Test.roc");
let src_dir = Path::new("fake/test/path");
@ -58,7 +94,6 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
}
let exposed_types = MutMap::default();
let ptr_bytes = 4;
let loaded = roc_load::file::load_and_monomorphize_from_str(
arena,
filename,
@ -66,7 +101,7 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
stdlib,
src_dir,
exposed_types,
ptr_bytes,
roc_target::TargetInfo::default_wasm32(),
builtin_defs_map,
);
@ -81,32 +116,13 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
..
} = loaded;
// You can comment and uncomment this block out to get more useful information
// while you're working on the wasm backend!
{
// println!("=========== Procedures ==========");
// if PRETTY_PRINT_IR_SYMBOLS {
// println!("");
// for proc in procedures.values() {
// println!("{}", proc.to_pretty(200));
// }
// } else {
// println!("{:?}", procedures.values());
// }
// println!("=================================\n");
debug_assert_eq!(exposed_to_host.values.len(), 1);
// println!("=========== Interns ==========");
// println!("{:?}", interns);
// println!("=================================\n");
// println!("=========== Exposed ==========");
// println!("{:?}", exposed_to_host);
// println!("=================================\n");
}
debug_assert_eq!(exposed_to_host.len(), 1);
let exposed_to_host = exposed_to_host.keys().copied().collect::<MutSet<_>>();
let exposed_to_host = exposed_to_host
.values
.keys()
.copied()
.collect::<MutSet<_>>();
let env = roc_gen_wasm::Env {
arena,
@ -114,94 +130,55 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
exposed_to_host,
};
let (mut wasm_module, main_fn_index) =
roc_gen_wasm::build_module_help(&env, &mut interns, procedures).unwrap();
let (mut module, called_preload_fns, main_fn_index) =
roc_gen_wasm::build_module_without_test_wrapper(
&env,
&mut interns,
preload_bytes,
procedures,
);
T::insert_test_wrapper(arena, &mut wasm_module, TEST_WRAPPER_NAME, main_fn_index);
T::insert_test_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index);
let mut module_bytes = std::vec::Vec::with_capacity(4096);
wasm_module.serialize_mut(&mut module_bytes);
// Export the initialiser function for refcount tests
let init_refcount_bytes = INIT_REFCOUNT_NAME.as_bytes();
let init_refcount_idx = module.names.functions[init_refcount_bytes];
module.export.append(Export {
name: arena.alloc_slice_copy(init_refcount_bytes),
ty: ExportType::Func,
index: init_refcount_idx,
});
// now, do wasmer stuff
module.remove_dead_preloads(env.arena, called_preload_fns);
use wasmer::{Instance, Module, Store};
let mut app_module_bytes = std::vec::Vec::with_capacity(module.size());
module.serialize(&mut app_module_bytes);
app_module_bytes
}
fn save_wasm_file(app_module_bytes: &[u8], build_dir_hash: u64) {
let debug_dir_str = format!("/tmp/roc/gen_wasm/{:016x}", build_dir_hash);
let debug_dir_path = Path::new(&debug_dir_str);
let final_wasm_file = debug_dir_path.join("final.wasm");
std::fs::create_dir_all(debug_dir_path).unwrap();
std::fs::write(&final_wasm_file, app_module_bytes).unwrap();
println!(
"Debug command:\n\twasm-objdump -dx {}",
final_wasm_file.to_str().unwrap()
);
}
fn load_bytes_into_runtime(bytes: Vec<u8>) -> wasmer::Instance {
use wasmer::{Module, Store};
use wasmer_wasi::WasiState;
let store = Store::default();
// Keep the final .wasm file for debugging with wasm-objdump or wasm2wat
const DEBUG_WASM_FILE: bool = false;
let wasmer_module = {
let tmp_dir: TempDir; // directory for normal test runs, deleted when dropped
let debug_dir: String; // persistent directory for debugging
let wasm_build_dir: &Path = if DEBUG_WASM_FILE {
// Directory name based on a hash of the Roc source
let mut hash_state = DefaultHasher::new();
src.hash(&mut hash_state);
let src_hash = hash_state.finish();
debug_dir = format!("/tmp/roc/gen_wasm/{:016x}", src_hash);
std::fs::create_dir_all(&debug_dir).unwrap();
println!(
"Debug command:\n\twasm-objdump -sdx {}/final.wasm",
&debug_dir
);
Path::new(&debug_dir)
} else {
tmp_dir = tempdir().unwrap();
tmp_dir.path()
};
let final_wasm_file = wasm_build_dir.join("final.wasm");
let app_o_file = wasm_build_dir.join("app.o");
let test_platform_o = format!("{}/{}.o", TEST_OUT_DIR, PLATFORM_FILENAME);
// write the module to a file so the linker can access it
std::fs::write(&app_o_file, &module_bytes).unwrap();
let args = &[
"wasm-ld",
// input files
app_o_file.to_str().unwrap(),
bitcode::BUILTINS_WASM32_OBJ_PATH,
&test_platform_o,
LIBC_A_FILE,
// output
"-o",
final_wasm_file.to_str().unwrap(),
// we don't define `_start`
"--no-entry",
// If you only specify test_wrapper, it will stop at the call to UserApp_main_1
// But if you specify both exports, you get all the dependencies.
//
// It seems that it will not write out an export you didn't explicitly specify,
// even if it's a dependency of another export!
// In our case we always export main and test_wrapper so that's OK.
"--export",
"test_wrapper",
"--export",
"#UserApp_main_1",
];
let linker_output = std::process::Command::new("zig")
.args(args)
.output()
.unwrap();
if !linker_output.status.success() {
print!("\nLINKER FAILED\n");
for arg in args {
print!("{} ", arg);
}
println!("\n{}", std::str::from_utf8(&linker_output.stdout).unwrap());
println!("{}", std::str::from_utf8(&linker_output.stderr).unwrap());
}
Module::from_file(&store, &final_wasm_file).unwrap()
};
let wasmer_module = Module::new(&store, &bytes).unwrap();
// First, we create the `WasiEnv`
use wasmer_wasi::WasiState;
let mut wasi_env = WasiState::new("hello").finalize().unwrap();
// Then, we get the import object related to our WASI
@ -210,11 +187,11 @@ pub fn helper_wasm<'a, T: Wasm32TestResult>(
.import_object(&wasmer_module)
.unwrap_or_else(|_| wasmer::imports!());
Instance::new(&wasmer_module, &import_object).unwrap()
wasmer::Instance::new(&wasmer_module, &import_object).unwrap()
}
#[allow(dead_code)]
pub fn assert_wasm_evals_to_help<T>(src: &str, phantom: T) -> Result<T, String>
pub fn assert_wasm_evals_to_help<T>(src: &str, phantom: PhantomData<T>) -> Result<T, String>
where
T: FromWasm32Memory + Wasm32TestResult,
{
@ -223,7 +200,8 @@ where
// NOTE the stdlib must be in the arena; just taking a reference will segfault
let stdlib = arena.alloc(roc_builtins::std::standard_stdlib());
let instance = crate::helpers::wasm::helper_wasm(&arena, src, stdlib, &phantom);
let instance =
crate::helpers::wasm::compile_and_load(&arena, src, stdlib, phantom, TestType::Evaluate);
let memory = instance.exports.get_memory(MEMORY_NAME).unwrap();
@ -238,9 +216,15 @@ where
};
if false {
println!("test_wrapper returned 0x{:x}", address);
println!("Stack:");
crate::helpers::wasm::debug_memory_hex(memory, address, std::mem::size_of::<T>());
}
if false {
println!("Heap:");
// Manually provide address and size based on printf in wasm_test_platform.c
crate::helpers::wasm::debug_memory_hex(memory, 0x11440, 24);
}
let output = <T as FromWasm32Memory>::decode(memory, address as u32);
Ok(output)
@ -248,6 +232,73 @@ where
}
}
#[allow(dead_code)]
pub fn assert_wasm_refcounts_help<T>(
src: &str,
phantom: PhantomData<T>,
num_refcounts: usize,
) -> Result<Vec<u32>, String>
where
T: FromWasm32Memory + Wasm32TestResult,
{
let arena = bumpalo::Bump::new();
// NOTE the stdlib must be in the arena; just taking a reference will segfault
let stdlib = arena.alloc(roc_builtins::std::standard_stdlib());
let instance =
crate::helpers::wasm::compile_and_load(&arena, src, stdlib, phantom, TestType::Refcount);
let memory = instance.exports.get_memory(MEMORY_NAME).unwrap();
let expected_len = num_refcounts as i32;
let init_refcount_test = instance.exports.get_function(INIT_REFCOUNT_NAME).unwrap();
let init_result = init_refcount_test.call(&[wasmer::Value::I32(expected_len)]);
let refcount_vector_addr = match init_result {
Err(e) => return Err(format!("{:?}", e)),
Ok(result) => match result[0] {
wasmer::Value::I32(a) => a,
_ => panic!(),
},
};
// Run the test
let test_wrapper = instance.exports.get_function(TEST_WRAPPER_NAME).unwrap();
match test_wrapper.call(&[]) {
Err(e) => return Err(format!("{:?}", e)),
Ok(_) => {}
}
// Check we got the right number of refcounts
let refcount_vector_len: WasmPtr<i32> = WasmPtr::new(refcount_vector_addr as u32);
let actual_len = refcount_vector_len.deref(memory).unwrap().get();
if actual_len != expected_len {
panic!("Expected {} refcounts but got {}", expected_len, actual_len);
}
// Read the actual refcount values
let refcount_ptr_array: WasmPtr<WasmPtr<i32>, wasmer::Array> =
WasmPtr::new(4 + refcount_vector_addr as u32);
let refcount_ptrs: &[Cell<WasmPtr<i32>>] = refcount_ptr_array
.deref(memory, 0, num_refcounts as u32)
.unwrap();
let mut refcounts = Vec::with_capacity(num_refcounts);
for i in 0..num_refcounts {
let rc_ptr = refcount_ptrs[i].get();
let rc = if rc_ptr.offset() == 0 {
// RC pointer has been set to null, which means the value has been freed.
// In tests, we simply represent this as zero refcount.
0
} else {
let rc_encoded = rc_ptr.deref(memory).unwrap().get();
(rc_encoded - i32::MIN + 1) as u32
};
refcounts.push(rc);
}
Ok(refcounts)
}
/// Print out hex bytes of the test result, and a few words on either side
/// Can be handy for debugging misalignment issues etc.
pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) {
@ -257,12 +308,17 @@ pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) {
};
let extra_words = 2;
let offset = (address as usize) / 4;
let start = offset - extra_words;
let end = offset + (size / 4) + extra_words;
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 == offset { "*" } else { " " };
let result_marker = if index >= result_start && index < result_end {
"|"
} else {
" "
};
println!(
"{:x} {} {:08x}",
index * 4,
@ -270,12 +326,13 @@ pub fn debug_memory_hex(memory: &Memory, address: i32, size: usize) {
memory_words[index]
);
}
println!();
}
#[allow(unused_macros)]
macro_rules! assert_wasm_evals_to {
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
let phantom = <$ty>::default();
let phantom = std::marker::PhantomData;
match $crate::helpers::wasm::assert_wasm_evals_to_help::<$ty>($src, phantom) {
Err(msg) => panic!("{:?}", msg),
Ok(actual) => {
@ -316,7 +373,29 @@ pub fn identity<T>(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_refcounts: expr) => {{
let phantom = std::marker::PhantomData;
let num_refcounts = $expected_refcounts.len();
let result =
$crate::helpers::wasm::assert_wasm_refcounts_help::<$ty>($src, phantom, num_refcounts);
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_wasm_evals_to;
#[allow(unused_imports)]
pub(crate) use assert_refcounts;

View file

@ -14,25 +14,27 @@ pub trait Wasm32TestResult {
wrapper_name: &str,
main_function_index: u32,
) {
let index = module.code.code_builders.len() as u32;
let index = module.import.function_count
+ module.code.preloaded_count
+ module.code.code_builders.len() as u32;
module.add_function_signature(Signature {
param_types: Vec::with_capacity_in(0, arena),
ret_type: Some(ValueType::I32),
});
module.export.entries.push(Export {
name: wrapper_name.to_string(),
module.export.append(Export {
name: arena.alloc_slice_copy(wrapper_name.as_bytes()),
ty: ExportType::Func,
index,
});
let symbol_table = module.linking.symbol_table_mut();
symbol_table.push(SymInfo::Function(WasmObjectSymbol::Defined {
let linker_symbol = SymInfo::Function(WasmObjectSymbol::Defined {
flags: 0,
index,
name: wrapper_name.to_string(),
}));
});
module.linking.symbol_table.push(linker_symbol);
let mut code_builder = CodeBuilder::new(arena);
Self::build_wrapper_body(&mut code_builder, main_function_index);
@ -57,7 +59,7 @@ macro_rules! build_wrapper_body_primitive {
code_builder.$store_instruction($align, 0);
code_builder.get_local(frame_pointer_id);
code_builder.build_fn_header(local_types, frame_size, frame_pointer);
code_builder.build_fn_header_and_footer(local_types, frame_size, frame_pointer);
}
};
}
@ -84,7 +86,7 @@ fn build_wrapper_body_stack_memory(
code_builder.get_local(local_id);
code_builder.call(main_function_index, main_symbol_index, 0, true);
code_builder.get_local(local_id);
code_builder.build_fn_header(local_types, size as i32, frame_pointer);
code_builder.build_fn_header_and_footer(local_types, size as i32, frame_pointer);
}
macro_rules! wasm_test_result_stack_memory {
@ -114,7 +116,7 @@ wasm_test_result_primitive!(u64, i64_store, Align::Bytes8);
wasm_test_result_primitive!(i64, i64_store, Align::Bytes8);
wasm_test_result_primitive!(usize, i32_store, Align::Bytes4);
wasm_test_result_primitive!(f32, f32_store, Align::Bytes8);
wasm_test_result_primitive!(f32, f32_store, Align::Bytes4);
wasm_test_result_primitive!(f64, f64_store, Align::Bytes8);
wasm_test_result_stack_memory!(u128);
@ -147,7 +149,7 @@ impl Wasm32TestResult for () {
let main_symbol_index = main_function_index;
code_builder.call(main_function_index, main_symbol_index, 0, false);
code_builder.get_global(0);
code_builder.build_fn_header(&[], 0, None);
code_builder.build_fn_header_and_footer(&[], 0, None);
}
}

View file

@ -1,31 +1,146 @@
#include <stdio.h>
// If any printf is included for compilation, even if unused, test runs take 50% longer
#define DEBUG 0
// Makes test runs take 50% longer, due to linking
#define ENABLE_PRINTF 0
void *roc_alloc(size_t size, unsigned int alignment) { return malloc(size); }
typedef struct
{
size_t length;
size_t *elements[]; // flexible array member
} Vector;
// Globals for refcount testing
Vector *rc_pointers;
size_t rc_pointers_capacity;
// The rust test passes us the max number of allocations it expects to make,
// and we tell it where we're going to write the refcount pointers.
// It won't actually read that memory until later, when the test is done.
Vector *init_refcount_test(size_t capacity)
{
rc_pointers_capacity = capacity;
rc_pointers = malloc((1 + capacity) * sizeof(size_t *));
rc_pointers->length = 0;
for (size_t i = 0; i < capacity; ++i)
rc_pointers->elements[i] = NULL;
return rc_pointers;
}
#if ENABLE_PRINTF
#define ASSERT(condition, format, ...) \
if (!(condition)) \
{ \
printf("ASSERT FAILED: " #format "\n", __VA_ARGS__); \
abort(); \
}
#else
#define ASSERT(condition, format, ...) \
if (!(condition)) \
abort();
#endif
size_t *alloc_ptr_to_rc_ptr(void *ptr, unsigned int alignment)
{
size_t alloc_addr = (size_t)ptr;
size_t rc_addr = alloc_addr + alignment - sizeof(size_t);
return (size_t *)rc_addr;
}
//--------------------------
void *roc_alloc(size_t size, unsigned int alignment)
{
void *allocated = malloc(size);
if (rc_pointers)
{
ASSERT(alignment >= sizeof(size_t), "alignment %zd != %zd", alignment, sizeof(size_t));
size_t num_alloc = rc_pointers->length + 1;
ASSERT(num_alloc <= rc_pointers_capacity, "Too many allocations %zd > %zd", num_alloc, rc_pointers_capacity);
size_t *rc_ptr = alloc_ptr_to_rc_ptr(allocated, alignment);
rc_pointers->elements[rc_pointers->length] = rc_ptr;
rc_pointers->length++;
}
#if ENABLE_PRINTF
if (!allocated)
{
fprintf(stderr, "roc_alloc failed\n");
exit(1);
}
else
{
printf("roc_alloc allocated %d bytes with alignment %d at %p\n", size, alignment, allocated);
}
#endif
return allocated;
}
//--------------------------
void *roc_realloc(void *ptr, size_t new_size, size_t old_size,
unsigned int alignment)
{
#if ENABLE_PRINTF
printf("roc_realloc reallocated %p from %d to %d with alignment %zd\n",
ptr, old_size, new_size, alignment);
#endif
return realloc(ptr, new_size);
}
void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); }
//--------------------------
void roc_dealloc(void *ptr, unsigned int alignment)
{
if (rc_pointers)
{
// Null out the entry in the test array to indicate that it was freed
// Then even if malloc reuses the space, everything still works
size_t *rc_ptr = alloc_ptr_to_rc_ptr(ptr, alignment);
int i = 0;
for (; i < rc_pointers->length; ++i)
{
if (rc_pointers->elements[i] == rc_ptr)
{
rc_pointers->elements[i] = NULL;
break;
}
}
int was_found = i < rc_pointers->length;
ASSERT(was_found, "RC pointer not found %p", rc_ptr);
}
#if ENABLE_PRINTF
printf("roc_dealloc deallocated %p with alignment %zd\n", ptr, alignment);
#endif
free(ptr);
}
//--------------------------
void roc_panic(void *ptr, unsigned int alignment)
{
#if DEBUG
#if ENABLE_PRINTF
char *msg = (char *)ptr;
fprintf(stderr,
"Application crashed with message\n\n %s\n\nShutting down\n", msg);
#endif
exit(1);
abort();
}
//--------------------------
void *roc_memcpy(void *dest, const void *src, size_t n)
{
return memcpy(dest, src, n);
}
void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); }
//--------------------------
void *roc_memset(void *str, int c, size_t n)
{
return memset(str, c, n);
}