2 Commits

Author SHA1 Message Date
Oleksandr Kozachuk 9b10723a95 Merge pull request #4 from ok2/fix/web-error-messages
CI / check (push) Has been cancelled
fix(web): surface Forth messages from host-word throws
2026-08-06 20:46:11 +02:00
Oleksandr Kozachuk 4769987b20 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.
2026-08-06 20:45:27 +02:00
6 changed files with 41 additions and 8 deletions
+16
View File
@@ -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/), 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). 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 ## [0.2.3] - 2026-08-06
### Fixed ### Fixed
Generated
+3 -3
View File
@@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "wafer" name = "wafer"
version = "0.2.3" version = "0.2.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -1600,7 +1600,7 @@ dependencies = [
[[package]] [[package]]
name = "wafer-core" name = "wafer-core"
version = "0.2.3" version = "0.2.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"insta", "insta",
@@ -1615,7 +1615,7 @@ dependencies = [
[[package]] [[package]]
name = "wafer-web" name = "wafer-web"
version = "0.2.3" version = "0.2.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"js-sys", "js-sys",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.2.3" version = "0.2.4"
edition = "2024" edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/ok2/wafer" repository = "https://github.com/ok2/wafer"
+1 -1
View File
@@ -9,7 +9,7 @@ license.workspace = true
workspace = true workspace = true
[dependencies] [dependencies]
wafer-core = { path = "../core", version = "0.2.3" } wafer-core = { path = "../core", version = "0.2.4" }
wasmtime = { workspace = true } wasmtime = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
+1 -1
View File
@@ -12,7 +12,7 @@ workspace = true
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [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" wasm-bindgen = "0.2"
js-sys = "0.3" js-sys = "0.3"
send_wrapper = { workspace = true } send_wrapper = { workspace = true }
+19 -2
View File
@@ -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 { impl HostAccess for WebHostAccess {
fn mem_read_i32(&mut self, addr: u32) -> i32 { fn mem_read_i32(&mut self, addr: u32) -> i32 {
let view = js_sys::Int32Array::new(&self.buffer()); let view = js_sys::Int32Array::new(&self.buffer());
@@ -134,7 +151,7 @@ impl HostAccess for WebHostAccess {
.dyn_into() .dyn_into()
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not a function"))?; .map_err(|_| anyhow::anyhow!("table entry {fn_index} is not a function"))?;
func.call0(&JsValue::NULL) func.call0(&JsValue::NULL)
.map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?; .map_err(|e| call_error(fn_index, &e))?;
Ok(()) Ok(())
} }
} }
@@ -406,7 +423,7 @@ impl Runtime for WebRuntime {
.dyn_into() .dyn_into()
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not callable"))?; .map_err(|_| anyhow::anyhow!("table entry {fn_index} is not callable"))?;
func.call0(&JsValue::NULL) func.call0(&JsValue::NULL)
.map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?; .map_err(|e| call_error(fn_index, &e))?;
Ok(()) Ok(())
} }