mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-27 13:59:08 +00:00
400 lines
13 KiB
HTML
400 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;
|
|
}
|
|
input,
|
|
button {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
padding: 4px 12px;
|
|
}
|
|
select {
|
|
font-size: 18px;
|
|
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_function_name</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>
|
|
If your test is from <code>gen_refcount.rs</code>
|
|
</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>$main</code>
|
|
</li>
|
|
<li>
|
|
Chrome DevTools has a handy 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">
|
|
<label>
|
|
<input type="checkbox" id="refcount-test">
|
|
Check box if test is from <code>gen_refcount.rs</code>
|
|
</label>
|
|
</div>
|
|
<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-run">RUN</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<script>
|
|
const file_input = document.getElementById("wasm-file");
|
|
const refcount_checkbox = document.getElementById("refcount-test");
|
|
const button = document.getElementById("button-run");
|
|
const error_box = document.getElementById("error");
|
|
|
|
if (localStorage.getItem("refcount-test")) {
|
|
refcount_checkbox.checked = true;
|
|
}
|
|
refcount_checkbox.onchange = function (ev) {
|
|
if (ev.target.checked) {
|
|
localStorage.setItem("refcount-test", "true");
|
|
} else {
|
|
localStorage.removeItem("refcount-test");
|
|
}
|
|
}
|
|
|
|
button.onclick = function () {
|
|
if (refcount_checkbox.checked) {
|
|
runRefcountTest();
|
|
} else {
|
|
runExpressionTest();
|
|
}
|
|
}
|
|
|
|
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 $main
|
|
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 $main
|
|
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}`);
|
|
}
|
|
|
|
// send_panic_msg_to_rust (i32, i32) => {}
|
|
function send_panic_msg_to_rust(msg, length) {
|
|
throw new Error("Wasm hit a panic");
|
|
}
|
|
|
|
// 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 {
|
|
env: {
|
|
send_panic_msg_to_rust,
|
|
},
|
|
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>
|