roc/crates/compiler/test_gen/src/helpers/debug-wasm-test.html

372 lines
13 KiB
HTML

<html>
<head>
<style>
body {
background-color: #111;
color: #ccc;
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: #aaa;
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;
const STDOUT = 1;
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) {
if (fd === STDOUT) {
console.log(string_buffer);
} else {
console.error(string_buffer);
}
}
return 0;
}
// proc_exit : (i32) -> nil
function proc_exit(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>