From 4769987b200414531fbeb5de57d396eff64450e7 Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk <201152+ok2@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:45:27 +0200 Subject: [PATCH] fix(web): surface Forth messages from host-word throws A host word signals failure by throwing across the JS boundary, and the browser runtime reported that exception with its Debug form, so an empty-stack RESIZE surfaced as call_func(134) failed: JsValue(Error: Stack underflow ...) with the engine's JS stack trace glued on. The thrown message IS the Forth message, so take it verbatim: 'Stack underflow', the same text the native CLI prints. Exceptions without a message keep the call context -- those are genuine runtime faults, not Forth throws. CATCH is unaffected: it reads the throw code from its own channel rather than parsing messages. Verified against a fresh VM in Node (initSync + WaferRepl): host-word underflow, compiled-guard underflow, THROW, unknown word and ' RESIZE CATCH . all match the native CLI. Ships as v0.2.4. --- CHANGELOG.md | 16 ++++++++++++++++ Cargo.lock | 6 +++--- Cargo.toml | 2 +- crates/cli/Cargo.toml | 2 +- crates/web/Cargo.toml | 2 +- crates/web/src/runtime_web.rs | 21 +++++++++++++++++++-- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f329e4b..bc4b09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to WAFER are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.4] - 2026-08-06 + +### Fixed + +- **Errors from host words in the browser build read like Forth errors + again.** A host word signals failure by throwing across the JS + boundary, and the browser runtime reported the exception with its + `Debug` form, so an empty-stack `RESIZE` came back as + `call_func(134) failed: JsValue(Error: Stack underflow ...)` trailed by + an engine stack trace. The thrown message is the Forth message, so it + is now surfaced verbatim — `Stack underflow`, exactly what the native + CLI prints. Exceptions that carry no message keep the call context, + since those are genuine runtime faults rather than Forth throws. + `CATCH` was never affected: it reads the throw code from its own + channel, not from the message. + ## [0.2.3] - 2026-08-06 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 085ca10..e1e08d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wafer" -version = "0.2.3" +version = "0.2.4" dependencies = [ "anyhow", "clap", @@ -1600,7 +1600,7 @@ dependencies = [ [[package]] name = "wafer-core" -version = "0.2.3" +version = "0.2.4" dependencies = [ "anyhow", "insta", @@ -1615,7 +1615,7 @@ dependencies = [ [[package]] name = "wafer-web" -version = "0.2.3" +version = "0.2.4" dependencies = [ "anyhow", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index e40ac09..53bc3f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.2.3" +version = "0.2.4" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/ok2/wafer" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 8ec3a57..a34c4e4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true workspace = true [dependencies] -wafer-core = { path = "../core", version = "0.2.3" } +wafer-core = { path = "../core", version = "0.2.4" } wasmtime = { workspace = true } anyhow = { workspace = true } clap = { version = "4", features = ["derive"] } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index e12f155..ffde45e 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -12,7 +12,7 @@ workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -wafer-core = { path = "../core", version = "0.2.3", default-features = false, features = ["crypto"] } +wafer-core = { path = "../core", version = "0.2.4", default-features = false, features = ["crypto"] } wasm-bindgen = "0.2" js-sys = "0.3" send_wrapper = { workspace = true } diff --git a/crates/web/src/runtime_web.rs b/crates/web/src/runtime_web.rs index a25ce73..67ff5e6 100644 --- a/crates/web/src/runtime_web.rs +++ b/crates/web/src/runtime_web.rs @@ -38,6 +38,23 @@ impl WebHostAccess { } } +/// An exception on its way back out of compiled code. Host words rethrow the +/// Forth message (`Stack underflow`, an `ABORT"` text, a `THROW` description), +/// so surface exactly that and nothing else — the JS `Error` carries the whole +/// engine stack in its message, which is noise to a Forth programmer. Anything +/// without a message is a genuine runtime fault and keeps the call context. +fn call_error(fn_index: u32, e: &JsValue) -> anyhow::Error { + match Reflect::get(e, &"message".into()) + .ok() + .and_then(|m| m.as_string()) + .and_then(|m| m.lines().next().map(str::trim).map(str::to_string)) + .filter(|m| !m.is_empty()) + { + Some(msg) => anyhow::anyhow!("{msg}"), + None => anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"), + } +} + impl HostAccess for WebHostAccess { fn mem_read_i32(&mut self, addr: u32) -> i32 { let view = js_sys::Int32Array::new(&self.buffer()); @@ -134,7 +151,7 @@ impl HostAccess for WebHostAccess { .dyn_into() .map_err(|_| anyhow::anyhow!("table entry {fn_index} is not a function"))?; func.call0(&JsValue::NULL) - .map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?; + .map_err(|e| call_error(fn_index, &e))?; Ok(()) } } @@ -406,7 +423,7 @@ impl Runtime for WebRuntime { .dyn_into() .map_err(|_| anyhow::anyhow!("table entry {fn_index} is not callable"))?; func.call0(&JsValue::NULL) - .map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?; + .map_err(|e| call_error(fn_index, &e))?; Ok(()) }