diff --git a/crates/compiler/test_gen/src/helpers/wasm.rs b/crates/compiler/test_gen/src/helpers/wasm.rs index 09e5cb1b5ea..ae3a71a70d0 100644 --- a/crates/compiler/test_gen/src/helpers/wasm.rs +++ b/crates/compiler/test_gen/src/helpers/wasm.rs @@ -202,12 +202,12 @@ impl<'a> ImportDispatcher for TestDispatcher<'a> { 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 tag = arguments[1].expect_i32().unwrap(); + let panic_tag = arguments[1].expect_i32().unwrap(); let roc_msg = RocStr::decode(memory, msg_ptr as _); - let msg = match tag { - 0 => format!(r#"Roc failed with message: "{}""#, roc_msg), - 1 => format!(r#"User crash with message: "{}""#, roc_msg), - tag => format!(r#"Got an invald panic tag: "{}""#, tag), + let msg = match panic_tag { + 0 => format!(r#"Roc failed with message: "{roc_msg}""#), + 1 => format!(r#"User crash with message: "{roc_msg}""#), + tag => format!(r#"Got an invald panic tag: "{panic_tag}""#), }; panic!("{}", msg) } else { diff --git a/crates/compiler/test_gen/src/helpers/wasm_test_platform.c b/crates/compiler/test_gen/src/helpers/wasm_test_platform.c index ca27b4a4b9b..71f84352a55 100644 --- a/crates/compiler/test_gen/src/helpers/wasm_test_platform.c +++ b/crates/compiler/test_gen/src/helpers/wasm_test_platform.c @@ -124,11 +124,11 @@ void roc_dealloc(void *ptr, unsigned int alignment) //-------------------------- -extern void send_panic_msg_to_rust(void* msg, uint32_t tag_id); +extern void send_panic_msg_to_rust(void* msg, uint32_t panic_tag); -void roc_panic(void* msg, unsigned int tag_id) +void roc_panic(void* msg, unsigned int panic_tag) { - send_panic_msg_to_rust(msg, tag_id); + send_panic_msg_to_rust(msg, panic_tag); exit(101); } diff --git a/crates/repl_wasm/src/repl_platform.c b/crates/repl_wasm/src/repl_platform.c index 320c156c902..9e55b6201f4 100644 --- a/crates/repl_wasm/src/repl_platform.c +++ b/crates/repl_wasm/src/repl_platform.c @@ -54,8 +54,11 @@ void roc_dealloc(void *ptr, unsigned int alignment) //-------------------------- -void roc_panic(void *ptr, unsigned int alignment) +extern void send_panic_msg_to_js(void *ptr, unsigned int panic_tag); + +void roc_panic(void *ptr, unsigned int panic_tag) { + send_panic_msg_to_js(ptr, panic_tag); #if ENABLE_PRINTF char *msg = (char *)ptr; fprintf(stderr, diff --git a/www/public/repl/repl.js b/www/public/repl/repl.js index 7d454982098..c688e21833e 100644 --- a/www/public/repl/repl.js +++ b/www/public/repl/repl.js @@ -165,11 +165,51 @@ async function processInputQueue() { // Callbacks to JS from Rust // ---------------------------------------------------------------------------- +var ROC_PANIC_INFO = null; + +function send_panic_msg_to_js(rocstr_ptr, panic_tag) { + const { memory } = repl.app.exports; + + const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12); + const finalByte = rocStrBytes[11] + + let stringBytes = ""; + if (finalByte < 0) { + // small string + + // bitwise ops on negative JS numbers are weird. This clears the bit that we + // use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000` + const length = finalByte + 128; + stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length); + } else { + // big string + const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3); + const [ptr, len, _cap] = rocStrWords; + + const SEAMLESS_SLICE_BIT = 1 << 31; + const length = len & (~SEAMLESS_SLICE_BIT); + + stringBytes = new Uint8Array(memory.buffer, ptr, length); + } + + const decodedString = repl.textDecoder.decode(stringBytes); + + ROC_PANIC_INFO = { + msg: decodedString, + panic_tag: panic_tag, + }; +} + // Load Wasm code into the browser's virtual machine, so we can run it later. // This operation is async, so we call it before entering any code shared // with the command-line REPL, which is sync. async function js_create_app(wasm_module_bytes) { - const { instance } = await WebAssembly.instantiate(wasm_module_bytes); + const { instance } = await WebAssembly.instantiate(wasm_module_bytes, { + env: { + send_panic_msg_to_js: send_panic_msg_to_js, + } + }); + // Keep the instance alive so we can run it later from shared REPL code repl.app = instance; } @@ -180,13 +220,41 @@ function js_run_app() { // Run the user code, and remember the result address // We'll pass it to Rust in the next callback - repl.result_addr = wrapper(); + try { + repl.result_addr = wrapper(); + } catch (e) { + // an exception could be that roc_panic was invoked, + // or some other crash (likely a compiler bug) + if (ROC_PANIC_INFO === null) { + throw e; + } else { + // when roc_panic set an error message, display it + const { msg, panic_tag } = ROC_PANIC_INFO; + ROC_PANIC_INFO = null; + + console.error(format_roc_panic_message(msg, panic_tag)); + } + } // Tell Rust how much space to reserve for its copy of the app's memory buffer. // We couldn't know that size until we actually ran the app. return memory.buffer.byteLength; } +function format_roc_panic_message(msg, panic_tag) { + switch (panic_tag) { + case 0: { + return `Roc failed with message: "${msg}"`; + } + case 1: { + return `User crash with message: "${msg}"`; + } + default: { + return `Got an invalid panic tag: "${panic_tag}"`; + } + } +} + // After Rust has allocated space for the app's memory buffer, // we copy it, and return the result address too function js_get_result_and_memory(buffer_alloc_addr) {