Compare commits
4 Commits
9b10723a95
...
v0.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
| e6c10a6fa1 | |||
| e110ca9516 | |||
| 8e2fd0d7d4 | |||
| 69309006a2 |
@@ -5,6 +5,54 @@ 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: <name> (throw -14)`, the
|
||||
standard condition both reference engines give. A genuine typo still
|
||||
reports `unknown word`.
|
||||
|
||||
## [0.2.5] - 2026-08-06
|
||||
|
||||
### Added
|
||||
|
||||
- **`QUIT`** ( -- ) ( R: i\*x -- ), the CORE word that was missing: empty
|
||||
the return stack, enter interpretation state, hand the input source
|
||||
back to the user input device and return to the interpreter without a
|
||||
message. The data stack is deliberately left alone — that is the whole
|
||||
difference to `ABORT`, which the standard defines as "empty the data
|
||||
stack, then `QUIT`". It unwinds through nested `EVALUATE` and
|
||||
`INCLUDE`, abandoning them, and `SOURCE-ID` is restored to 0.
|
||||
|
||||
`CATCH` does **not** report it: `QUIT` rides throw code -56, which the
|
||||
interpreter treats as a return to the prompt rather than an exception.
|
||||
Both behaviours were checked against gforth 0.7.3 and SwiftForth
|
||||
`sf64`, which agree — `1 2 ' QUIT CATCH .` prints nothing and leaves
|
||||
`1 2` on the stack in all three engines.
|
||||
|
||||
The gap had gone unnoticed because the Forth 2012 test suite skips it
|
||||
by its own admission ("I HAVEN'T FIGURED OUT HOW TO TEST KEY, QUIT,
|
||||
ABORT, OR ABORT\""), and because `HELP`'s coverage lint compares the
|
||||
dictionary against the docs — a word absent from both looks complete.
|
||||
`docs/wafer-anki.txt` had been documenting `QUIT` as if it existed.
|
||||
|
||||
Note that `ABORT` was already correct: executing it while a definition
|
||||
is open does clear both stacks and return to interpretation state.
|
||||
Typing `ABORT` (or `QUIT`) into an unfinished definition compiles it
|
||||
rather than running it, exactly as in every other Forth; `[` is the
|
||||
word that gets you out.
|
||||
|
||||
## [0.2.4] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
Generated
+3
-3
@@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wafer"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1600,7 +1600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wafer-core"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"insta",
|
||||
@@ -1615,7 +1615,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wafer-web"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"js-sys",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ok2/wafer"
|
||||
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
wafer-core = { path = "../core", version = "0.2.4" }
|
||||
wafer-core = { path = "../core", version = "0.2.6" }
|
||||
wasmtime = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
+163
-1
@@ -180,6 +180,15 @@ enum PendingAction {
|
||||
DeclareLocalEnd,
|
||||
}
|
||||
|
||||
/// Forth 2012 throw code for QUIT (table 9.1). Unlike every other code it is
|
||||
/// 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;
|
||||
@@ -758,6 +767,22 @@ impl<R: Runtime> ForthVM<R> {
|
||||
self.compile_frames.clear();
|
||||
self.compiling_source.clear();
|
||||
self.source_capture_from = None;
|
||||
// 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 matches!(*tc, Some(QUIT_THROW | ABORT_THROW)) {
|
||||
*tc = None;
|
||||
drop(tc);
|
||||
self.rt.mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, 0);
|
||||
self.include_frames.clear();
|
||||
return Ok(());
|
||||
}
|
||||
drop(tc);
|
||||
return Err(self.describe_uncaught(e));
|
||||
}
|
||||
}
|
||||
@@ -1271,6 +1296,14 @@ impl<R: Runtime> ForthVM<R> {
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -4442,6 +4475,20 @@ impl<R: Runtime> ForthVM<R> {
|
||||
});
|
||||
self.register_host_primitive("_ABORT_Q_", false, func)?;
|
||||
|
||||
// QUIT ( -- ) ( R: i*x -- ) empty the return stack and return to the
|
||||
// interpreter. The data stack is deliberately untouched -- that is the
|
||||
// whole difference to ABORT, which is specified as "empty the data
|
||||
// stack, then QUIT". Unwinding rides the throw plumbing so nested
|
||||
// EVALUATE / INCLUDE frames are abandoned on the way out; the standard
|
||||
// code -56 tells `evaluate` and CATCH what this is.
|
||||
let throw_code = Arc::clone(&self.throw_code);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
ctx.set_rsp((RETURN_STACK_TOP as i32) as u32);
|
||||
*throw_code.lock().unwrap() = Some(QUIT_THROW);
|
||||
Err(anyhow::anyhow!("forth-throw"))
|
||||
});
|
||||
self.register_host_primitive("QUIT", false, func)?;
|
||||
|
||||
// BYE ( -- ) request REPL/driver exit.
|
||||
let bye = Arc::clone(&self.bye);
|
||||
let func: HostFn = Box::new(move |_ctx: &mut dyn HostAccess| {
|
||||
@@ -4519,9 +4566,16 @@ impl<R: Runtime> ForthVM<R> {
|
||||
ctx.set_dsp((new_sp as i32) as u32);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
// Check if this was a THROW (vs some other trap)
|
||||
let mut tc = throw_code_for_catch.lock().unwrap();
|
||||
// QUIT is not an exception: it unwinds past CATCH straight
|
||||
// to the interpreter, leaving both stacks as it found them
|
||||
// (verified against gforth and SwiftForth).
|
||||
if *tc == Some(QUIT_THROW) {
|
||||
drop(tc);
|
||||
return Err(e);
|
||||
}
|
||||
let code = tc.take().unwrap_or(-1);
|
||||
drop(tc);
|
||||
|
||||
@@ -9340,6 +9394,114 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// QUIT — Forth 2012 6.1.2050. Semantics checked against gforth 0.7.3
|
||||
// and SwiftForth sf64: the data stack survives, nothing is printed,
|
||||
// the rest of the input is abandoned, and CATCH does not see it.
|
||||
// ===================================================================
|
||||
|
||||
#[test]
|
||||
fn test_quit_keeps_data_stack_and_abandons_the_rest() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("1 2 QUIT 99 .").unwrap(); // not an error, and 99 never runs
|
||||
assert_eq!(vm.take_output(), "");
|
||||
assert_eq!(vm.data_stack(), vec![2, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit_from_inside_a_definition() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate(": T 7 QUIT 8 . ; 5 T 6 .").unwrap();
|
||||
assert_eq!(vm.take_output(), "");
|
||||
assert_eq!(vm.data_stack(), vec![7, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit_empties_the_return_stack() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate(": T 1 >R 2 >R QUIT ; T").unwrap();
|
||||
vm.evaluate("RDEPTH .").unwrap();
|
||||
assert_eq!(vm.take_output(), "0 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit_is_not_caught_by_catch() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("1 2 ' QUIT CATCH .").unwrap();
|
||||
assert_eq!(vm.take_output(), "", "CATCH must not report QUIT");
|
||||
assert_eq!(vm.data_stack(), vec![2, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit_leaves_compile_mode_when_it_executes() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate(": BOOM QUIT ; IMMEDIATE").unwrap();
|
||||
vm.evaluate("9 : FOO 1 2 BOOM").unwrap();
|
||||
assert!(!vm.is_compiling(), "QUIT enters interpretation state");
|
||||
assert_eq!(vm.data_stack(), vec![9], "the data stack is left alone");
|
||||
vm.evaluate(": SQ DUP * ; 9 SQ .").unwrap(); // the VM is usable again
|
||||
assert_eq!(vm.take_output(), "81 ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit_inside_evaluate_restores_user_input_source() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("S\" 5 QUIT 6 .\" EVALUATE 7 .").unwrap();
|
||||
assert_eq!(vm.take_output(), "", "both the string and the line stop");
|
||||
assert_eq!(vm.data_stack(), vec![5]);
|
||||
vm.evaluate("SOURCE-ID .").unwrap();
|
||||
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::<NativeRuntime>::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::<NativeRuntime>::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::<NativeRuntime>::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::<NativeRuntime>::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::<NativeRuntime>::new().unwrap();
|
||||
let err = vm.evaluate("NOSUCHWORD").unwrap_err().to_string();
|
||||
assert!(err.contains("unknown word"), "{err}");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// New words: SOURCE
|
||||
// ===================================================================
|
||||
|
||||
@@ -716,6 +716,11 @@ pub const WORD_DOCS: &[(&str, &str, &str)] = &[
|
||||
"Read a line of input (unsupported here).",
|
||||
),
|
||||
("ABORT", "( i*x -- )", "Empty the stacks and abort."),
|
||||
(
|
||||
"QUIT",
|
||||
"( -- ) ( R: i*x -- )",
|
||||
"Empty the return stack, return to the interpreter; data stack kept.",
|
||||
),
|
||||
(
|
||||
"ABORT\"",
|
||||
"( flag -- )",
|
||||
|
||||
@@ -468,6 +468,11 @@ fn programs() -> Vec<Program> {
|
||||
expected: "-1 \n-1 \n42 \n-1 \n",
|
||||
category: Category::Definitions,
|
||||
},
|
||||
// QUIT is deliberately absent from this corpus: what it abandons is
|
||||
// "the input source", and each engine here is fed differently (wafer
|
||||
// line by line, gforth from a file, sf64 from a prompting stdin), so
|
||||
// a comparison would measure the harness. Its semantics are pinned by
|
||||
// the QUIT tests in outer.rs, checked by hand against both engines.
|
||||
// -- Strings --
|
||||
Program {
|
||||
name: "s-quote-type",
|
||||
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wafer-core = { path = "../core", version = "0.2.4", 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 }
|
||||
|
||||
Reference in New Issue
Block a user