diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff40c2..85847ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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.6] - 2026-08-07 + +### Fixed + +- **An uncaught `ABORT` no longer prints anything.** It used to report + `ABORT (throw -1)`, but the standard defines `ABORT` as "empty the data + stack and perform the function of `QUIT`", and `QUIT` displays no + message. gforth and SwiftForth are both silent here. `CATCH` still + reports -1 as before, and `ABORT"` still prints its text — that is a + different word with a different code (-2). +- **Compile-only words used in interpretation state name the condition.** + `ABORT"`, `IF`, `THEN`, `LOOP`, `LITERAL`, `RECURSE` and the rest of + the compile-time constructs claimed to be an `unknown word`, which is + actively misleading for a word the system obviously knows. They now + report `interpreting a compile-only word: (throw -14)`, the + standard condition both reference engines give. A genuine typo still + reports `unknown word`. + ## [0.2.5] - 2026-08-06 ### Added diff --git a/Cargo.lock b/Cargo.lock index 28041b0..45f2cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wafer" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anyhow", "clap", @@ -1600,7 +1600,7 @@ dependencies = [ [[package]] name = "wafer-core" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anyhow", "insta", @@ -1615,7 +1615,7 @@ dependencies = [ [[package]] name = "wafer-web" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anyhow", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index cfe514c..440d1cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.2.5" +version = "0.2.6" 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 7efc328..7a4e652 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.5" } +wafer-core = { path = "../core", version = "0.2.6" } wasmtime = { workspace = true } anyhow = { workspace = true } clap = { version = "4", features = ["derive"] } diff --git a/crates/core/src/outer.rs b/crates/core/src/outer.rs index 1dbebbb..6952bff 100644 --- a/crates/core/src/outer.rs +++ b/crates/core/src/outer.rs @@ -184,6 +184,11 @@ enum PendingAction { /// not an exception: CATCH lets it through and the interpreter reports nothing. const QUIT_THROW: i32 = -56; +/// Forth 2012 throw code for ABORT. CATCH sees it like any other exception, +/// but an uncaught one prints nothing: ABORT is specified as "empty the data +/// stack and perform the function of QUIT", and QUIT displays no message. +const ABORT_THROW: i32 = -1; + // Control-flow action codes for PendingAction::CompileControl const CTRL_IF: i32 = 1; const CTRL_ELSE: i32 = 2; @@ -762,13 +767,15 @@ impl ForthVM { self.compile_frames.clear(); self.compiling_source.clear(); self.source_capture_from = None; - // QUIT is not an error: the wipe above IS its "enter - // interpretation state", and the standard asks for the - // user input device back and no message at all. The rest + // QUIT and ABORT are not errors: the wipe above IS their + // "enter interpretation state", and the standard asks for + // the user input device back and no message at all (ABORT + // is defined as emptying the data stack and then doing + // QUIT; only ABORT" prints, and that is code -2). The rest // of this input -- and any EVALUATE / INCLUDE frame it // unwound through -- is abandoned by returning here. let mut tc = self.throw_code.lock().unwrap(); - if *tc == Some(QUIT_THROW) { + if matches!(*tc, Some(QUIT_THROW | ABORT_THROW)) { *tc = None; drop(tc); self.rt.mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, 0); @@ -1289,6 +1296,14 @@ impl ForthVM { return Ok(()); } + // Constructs the outer interpreter only knows how to compile. Forth + // 2012 leaves their interpretation semantics undefined and both gforth + // and SwiftForth name the standard condition, so say what is wrong + // instead of claiming the word does not exist. + if INTERPRETER_TOKENS.contains(&token.to_uppercase().as_str()) { + anyhow::bail!("interpreting a compile-only word: {token} (throw -14)"); + } + anyhow::bail!("unknown word: {token}"); } @@ -9438,6 +9453,55 @@ mod tests { assert_eq!(vm.take_output(), "0 ", "back to the user input device"); } + // =================================================================== + // ABORT reporting — gforth and sf64 both print nothing for an uncaught + // ABORT: it is specified as "empty the data stack and perform the + // function of QUIT", and QUIT displays no message. Only ABORT" prints. + // =================================================================== + + #[test] + fn test_abort_is_silent_and_abandons_the_rest() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate("1 2 ABORT 99 .").unwrap(); + assert_eq!(vm.take_output(), ""); + assert!(vm.data_stack().is_empty(), "ABORT empties the data stack"); + } + + #[test] + fn test_abort_is_still_catchable() { + // Unlike QUIT: CATCH reports -1 and restores the stack depth. + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate("1 2 ' ABORT CATCH .").unwrap(); + assert_eq!(vm.take_output(), "-1 "); + assert_eq!(vm.data_stack(), vec![2, 1]); + } + + #[test] + fn test_abort_quote_still_reports_its_text() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate(": T -1 ABORT\" oops\" ; T").unwrap_err(); + assert_eq!(err.to_string(), "oops"); + } + + #[test] + fn test_compile_only_words_say_so_in_interpret_mode() { + for word in ["ABORT\"", "IF", "THEN", "LOOP", "LITERAL", "RECURSE"] { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate(word).unwrap_err().to_string(); + assert!( + err.contains("compile-only word"), + "{word}: expected the standard condition, got {err}" + ); + } + } + + #[test] + fn test_compile_only_check_does_not_swallow_typos() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("NOSUCHWORD").unwrap_err().to_string(); + assert!(err.contains("unknown word"), "{err}"); + } + // =================================================================== // New words: SOURCE // =================================================================== diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 64bf6f8..71de7b5 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.5", default-features = false, features = ["crypto"] } +wafer-core = { path = "../core", version = "0.2.6", default-features = false, features = ["crypto"] } wasm-bindgen = "0.2" js-sys = "0.3" send_wrapper = { workspace = true }