1 Commits

Author SHA1 Message Date
ok fa7dadbdeb codegen: LOOP exits on index==limit (Forth 2012 boundary crossing) 2026-07-13 13:04:44 +02:00
31 changed files with 1233 additions and 5185 deletions
-3
View File
@@ -3,6 +3,3 @@
*.swp *.swp
.DS_Store .DS_Store
*.bk *.bk
# Local planning notes — never tracked
/plans/
-199
View File
@@ -1,199 +0,0 @@
# Changelog
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.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
- **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
- **Release builds of `wafer-web` no longer fail on proc-macro loading.**
Cargo strips debuginfo from release artifacts by default, and on macOS
that also strips the metadata proc-macro dylibs need to be loadable, so
`wasm-pack build --release` died with `can't find crate` for
`rustversion`, `thiserror_impl` and every other proc-macro. Build
scripts and proc-macros gain nothing from stripping, so
`[profile.release.build-override]` now exempts them; release binaries
stay stripped. Debug builds were never affected, which is why the test
suite stayed green while the browser REPL could not be built for
production.
- `wafer-web` and `wafer-cli` requested `wafer-core` version `0.2.1`
while the workspace had moved to `0.2.2`. The caret requirement still
resolved, so nothing broke, but the pin is now kept in step.
## [0.2.2] - 2026-08-06
### Added
- **SwiftForth-style input number conversion.** Punctuation (`,` `.` `+`
`/` `:` and an embedded `-`) anywhere after the leftmost digit now forces
double-cell conversion, so `12.34`, `1,234`, `12:30:45` and `2026-08-06`
all convert as doubles without a custom parser. Previously only a
trailing `.` worked and `1.5` was an "unknown word" error. The
punctuation is a double-cell marker, not a fractional point: every
spelling of `1234` (`1234.`, `123.4`, `.1234`) yields the same value.
- **`DPL`** ( -- addr ): digits to the right of the rightmost punctuation
character in the last converted number, negative when the token carried
none. Seeded at -1024 and bumped once per digit, matching `sf64`.
Together with `<# #>` this is how fixed-point input is scaled.
- **`NH`** ( -- addr ): the high-order cell dropped by a single-cell
conversion, so a token that overflows a cell can be recovered as a
double (`4000000000 NH @ D.`).
Verified token-for-token against SwiftForth `sf64`: DPL values, double
promotion and sign handling agree on every probed form. One deliberate
divergence — WAFER also accepts a sign before a base prefix (`-$FF`), which
`sf64` rejects; the Forth 2012 spelling `$-FF` works in both. A leading `+`
is punctuation rather than a sign in both engines, so `+7` is the double 7
with `DPL` = 1.
## [0.2.1] - 2026-08-06
### Fixed
- **The search order is now authoritative** (Forth 2012 §16.3.3): a word
whose wordlist is not in the search order is no longer findable.
Previously lookup fell back to the newest entry across all wordlists,
making word hiding impossible. Verified against gforth and SwiftForth,
and guarded by a cross-engine corpus program.
- **Host words validate their stack arguments.** Around 40 host-implemented
words (`RND-SEED`, `ACCEPT`, `RESIZE`, `ALLOCATE`, `FREE`, `SEARCH`,
`SUBSTITUTE`, `ROLL`, `M*`, `UM/MOD`, `SF@ SF! DF@ DF!`, `F. FE. FS. F~`,
`2R@`, and friends) performed raw stack-pointer arithmetic with no
underflow check — calling them on an empty stack silently corrupted the
stack pointer (the compiled-code guards from 0.2.0 do not cover host
words). All argument-taking host words now fail with a clean, CATCHable
underflow error, enforced by a class-wide regression test.
## [0.2.0] - 2026-08-06
The usability release: introspection, source files, honest errors, and a
safety net under every compiled word.
### Added
- **Stack guards in compiled code**: under/overflow checks at the
stack-pointer choke points of generated WASM. Faults THROW standard codes
(`-3`..`-6`, `-44`, `-45`), are CATCHable, and print standard messages
instead of silently corrupting memory. Default on; `wafer build` output
stays unguarded; `WAFER_STACK_GUARDS=0|1` overrides.
- **`SEE`**: source-level decompiler. Colon words (including everything in
`boot.fth`) show their captured verbatim source; data words show
synthesized definitions with current values (`9 VALUE X`,
`DEFER D ( IS DUP )`); primitives fall back to a readable IR dump —
`SEE` never dead-ends on a defined word.
- **`SEE-IR`**: post-optimization IR view with resolved callee names and
indented control flow — shows what the optimizer actually did.
- **`HELP`**: stack effect + one-line description for **every** word in a
fresh VM (dictionary words and outer-interpreter tokens alike); coverage
is enforced by a unit test, so an undocumented new word fails the build.
User words echo their leading `( n -- n )` comment.
- **`INCLUDE` / `INCLUDED`**: nestable source-file loading with cycle
detection, depth bound, paths relative to the including file, and
per-level `SOURCE-ID`. The loader is injected (CLI: filesystem; web:
defined error), so the core stays IO-free. `wafer prog.fth` now runs
through the same machinery.
- **`MARKER` extensions**: `REMEMBER` (re-runnable marker), `EMPTY` and
`GILD` (boot-state rollback and re-baselining). Marker rollback now also
restores search order, wordlists, `REPLACES` substitutions, `ABORT"`
texts, and captured word sources — enabling the `REMEMBER` + `INCLUDE`
edit-reload loop.
- **`WORDS`**: optional substring filter (`WORDS FLOAT`), word count, and
`WORDS ALL` — a grouped full view by wordlist plus internal words.
- **Return-stack introspection**: `.RS`, `RDEPTH`, `RP@`.
- **Tools**: `.S` honors `BASE`, `F.S`, `?`, bounds-checked `DUMP`, real
`BYE`, named `ORDER` output.
- **CLI REPL**: persistent history (XDG state dir, `0600`), dictionary-backed
tab completion, prefix history search on Up/Down, Ctrl-C clears the line.
- **Web REPL**: history persisted to localStorage, User Words palette,
`BASE` indicator in the stack bar.
- **Error reporting**: uncaught `THROW` codes map to standard messages;
`ABORT"` text prints only when uncaught; errors inside included files
carry `file.fth:line:` context; uncaught throws are typed
(`WaferError::UncaughtThrow`) for embedding consumers; compiled words
carry WASM name sections, so genuine traps name the faulting word
(`in CRASHER: wasm trap: out of bounds memory access`).
- **SwiftForth correctness lane**: the cross-engine program corpus can run
against sf64 as an oracle (`just compare-correctness`), alongside the
existing gforth lane and the sf64 performance lane.
### Fixed
- Multi-line command output in the CLI REPL starts on its own line
(inline `ok` echo only for single-line output).
- `.S` printed in decimal regardless of `BASE`.
- A bare interpreted `R>` underflowed silently (exposed by the new stack
guards; compliance baseline updated).
- `SPACES` with a negative count now outputs nothing, per Forth 2012
6.1.2230.
### Changed
- `wafer prog.fth` reports errors with `file:line` context and resolves
nested `INCLUDE`s relative to the file.
- Internal words (`_`-prefixed) are flagged in the dictionary and hidden
from `WORDS` and completion (`WORDS ALL` shows them).
- Dependencies upgraded across the board: wasmtime 43 → 47,
wasm-encoder/wasmparser 0.246 → 0.255, plus all semver-compatible
updates.
## [0.1.0] - 2026-08-04
Initial development line (untagged): Forth 2012 core with IR optimizer and
WASM codegen via wasm-encoder/wasmtime, ~300 words across Core, Double,
Float, String, Search-Order, Exception, and Tools word sets, Forth 2012
compliance suite, `CONSOLIDATE` whole-program recompilation, `wafer build`
AOT export (WASM / native / JS loader), browser REPL, SHA-1/256/512 words,
and cross-engine benchmark lanes against gforth and SwiftForth.
[0.2.1]: https://github.com/ok2/wafer/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/ok2/wafer/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/ok2/wafer/releases/tag/v0.1.0
+1 -1
View File
@@ -79,7 +79,7 @@ Handle in `interpret_token_immediate()` or `compile_token()` as a special case.
## Testing ## Testing
- Run `cargo test --workspace` before committing (currently 562 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto) - Run `cargo test --workspace` before committing (currently 431 unit + 1 benchmark + 11 compliance + 9 comparison)
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance` - Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
- Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison` - Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison`
- Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored` - Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
Generated
+634 -311
View File
File diff suppressed because it is too large Load Diff
+6 -14
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.2.5" version = "0.1.0"
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"
@@ -41,21 +41,13 @@ needless_collect = "warn"
or_fun_call = "warn" or_fun_call = "warn"
[workspace.dependencies] [workspace.dependencies]
wasm-encoder = "0.255" wasm-encoder = "0.246"
wasmparser = "0.255" wasmparser = "0.246"
wasmtime = "47" wasmtime = "43"
anyhow = "1" anyhow = "1"
thiserror = "2" thiserror = "2"
proptest = "1" proptest = "1"
insta = "1" insta = "1"
sha1 = "0.10" sha1 = "0.11"
sha2 = "0.10" sha2 = "0.11"
send_wrapper = "0.6" send_wrapper = "0.6"
# Cargo strips debuginfo from release artifacts by default, and on macOS that
# also strips the metadata proc-macro dylibs need to be loadable — release
# builds then fail with "can't find crate" for every proc-macro (rustversion,
# thiserror_impl, ...). Build scripts and proc-macros gain nothing from
# stripping, so exempt them; the release binaries stay stripped.
[profile.release.build-override]
strip = false
-15
View File
@@ -43,14 +43,6 @@ bench:
bench-opts: bench-opts:
cargo test -p wafer-core --test benchmark_report -- --nocapture --ignored cargo test -p wafer-core --test benchmark_report -- --nocapture --ignored
# Cross-engine performance report: WAFER vs gforth vs SwiftForth (sf64)
bench-compare:
CARGO_PROFILE_RELEASE_STRIP=none cargo test -p wafer-core --release --test comparison -- --nocapture --ignored performance_report
# Cross-engine correctness lanes: program corpus vs gforth + sf64 oracles
compare-correctness:
cargo test -p wafer-core --test comparison -- --nocapture --ignored compare_all_programs
# Check dependency licenses and advisories # Check dependency licenses and advisories
deny: deny:
cargo deny check cargo deny check
@@ -66,13 +58,6 @@ ci: fmt clippy deny test
check: check:
cargo check --workspace cargo check --workspace
# Install the wafer CLI (release build) and bat syntax highlighting.
# STRIP=none: Cargo's release default (strip = "debuginfo") emits dylibs that
# macOS 27's dyld rejects ("mis-aligned LINKEDIT string pool"), so proc macros
# fail to load during the build itself.
install: install-syntax
CARGO_PROFILE_RELEASE_STRIP=none cargo install --path crates/cli --locked
# Install bat syntax highlighting for WAFER / Forth # Install bat syntax highlighting for WAFER / Forth
install-syntax: install-syntax:
mkdir -p ~/.config/bat/syntaxes mkdir -p ~/.config/bat/syntaxes
+1 -2
View File
@@ -95,7 +95,7 @@ Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CON
## Testing ## Testing
```bash ```bash
# All tests (~570 currently passing) # All tests (~450 currently passing)
cargo test --workspace cargo test --workspace
# Forth 2012 compliance suite # Forth 2012 compliance suite
@@ -185,7 +185,6 @@ Over 200 words are implemented across the following categories:
| Strings | `COMPARE SEARCH SLITERAL REPLACES SUBSTITUTE UNESCAPE` | | Strings | `COMPARE SEARCH SLITERAL REPLACES SUBSTITUTE UNESCAPE` |
| Floating-Pt | `F+ F- F* F/ FABS FNEGATE FSQRT FSIN FCOS FTAN FEXP FLOG FMIN FMAX` and 55+ more | | Floating-Pt | `F+ F- F* F/ FABS FNEGATE FSQRT FSIN FCOS FTAN FEXP FLOG FMIN FMAX` and 55+ more |
| Case | `CASE OF ENDOF ENDCASE` | | Case | `CASE OF ENDOF ENDCASE` |
| Tools | `WORDS SEE SEE-IR HELP INCLUDE INCLUDED .S F.S ? DUMP MARKER REMEMBER EMPTY GILD BYE` |
## Web REPL ## Web REPL
+1 -1
View File
@@ -9,7 +9,7 @@ license.workspace = true
workspace = true workspace = true
[dependencies] [dependencies]
wafer-core = { path = "../core", version = "0.2.5" } wafer-core = { path = "../core", version = "0.1.0" }
wasmtime = { workspace = true } wasmtime = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
+58 -190
View File
@@ -137,9 +137,7 @@ fn cmd_build(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let source = std::fs::read_to_string(file)?; let source = std::fs::read_to_string(file)?;
// Exported modules are production artifacts: no stack guards by default let mut vm = ForthVM::<NativeRuntime>::new()?;
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(false))?;
vm.set_source_loader(fs_loader());
vm.set_recording(true); vm.set_recording(true);
vm.evaluate(&source)?; vm.evaluate(&source)?;
@@ -262,38 +260,18 @@ fn cmd_run(file: &str) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides
/// the per-command default (REPL/file execution on, build off).
fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
let mut cfg = wafer_core::config::WaferConfig::all();
cfg.codegen.stack_guards = match std::env::var("WAFER_STACK_GUARDS").ok().as_deref() {
Some("0") => false,
Some(_) => true,
None => default_guards,
};
cfg
}
/// Filesystem source loader for INCLUDE/INCLUDED.
fn fs_loader() -> Box<dyn Fn(&str) -> anyhow::Result<String> + Send + Sync> {
Box::new(|path| Ok(std::fs::read_to_string(path)?))
}
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit) /// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> { fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(true))?; let mut vm = ForthVM::<NativeRuntime>::new()?;
vm.set_source_loader(fs_loader());
match file { match file {
Some(file) => { Some(file) => {
// Through the include machinery: file:line error context and a let source = std::fs::read_to_string(file)?;
// base directory for nested INCLUDEs. vm.evaluate(&source)?;
let result = vm.include(file);
let output = vm.take_output(); let output = vm.take_output();
if !output.is_empty() { if !output.is_empty() {
print!("{output}"); print!("{output}");
} }
result?;
} }
None => { None => {
if !stdin_is_tty() { if !stdin_is_tty() {
@@ -307,17 +285,66 @@ fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
if !output.is_empty() { if !output.is_empty() {
print!("{output}"); print!("{output}");
} }
if vm.bye_requested() {
break;
}
} }
Err(e) => { Err(e) => {
eprintln!("Error: {e:#}"); eprintln!("Error: {e}");
} }
} }
} }
} else { } else {
run_repl(&mut vm)?; // Interactive REPL
println!(
"WAFER v{} - WebAssembly Forth Engine in Rust",
env!("CARGO_PKG_VERSION")
);
println!("Type BYE to exit.");
let mut rl = rustyline::DefaultEditor::new()?;
loop {
let prompt = if vm.is_compiling() { " ] " } else { "> " };
match rl.readline(prompt) {
Ok(line) => {
let trimmed = line.trim();
if trimmed.eq_ignore_ascii_case("BYE") {
break;
}
let _ = rl.add_history_entry(&line);
match vm.evaluate(&line) {
Ok(()) => {
let output = vm.take_output();
// PAGE (form feed) clears the terminal
if output.contains('\x0C') {
print!("\x1b[2J\x1b[H");
}
let output = output.replace('\x0C', "");
if !vm.is_compiling() {
// Move cursor back up to end of input line so
// output appears inline, like traditional Forth:
// > 2 2 + . 4 ok
let col = prompt.len() + line.len() + 1;
print!("\x1b[A\x1b[{col}G {output} ok");
println!();
} else if !output.is_empty() {
print!("{output}");
}
}
Err(e) => {
eprintln!("Error: {e}");
}
}
}
Err(
rustyline::error::ReadlineError::Interrupted
| rustyline::error::ReadlineError::Eof,
) => {
break;
}
Err(e) => {
eprintln!("Readline error: {e}");
break;
}
}
}
} }
} }
} }
@@ -330,162 +357,3 @@ fn stdin_is_tty() -> bool {
use std::io::IsTerminal; use std::io::IsTerminal;
std::io::stdin().is_terminal() std::io::stdin().is_terminal()
} }
/// Completes the token under the cursor against the live dictionary.
struct WaferHelper {
words: Vec<String>,
}
impl rustyline::completion::Completer for WaferHelper {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<String>)> {
let start = line[..pos]
.rfind(|c: char| c.is_whitespace())
.map_or(0, |i| i + 1);
let prefix = line[start..pos].to_ascii_uppercase();
let mut matches: Vec<String> = self
.words
.iter()
.filter(|w| w.to_ascii_uppercase().starts_with(&prefix))
.cloned()
.collect();
matches.sort();
matches.dedup();
Ok((start, matches))
}
}
impl rustyline::hint::Hinter for WaferHelper {
type Hint = String;
}
impl rustyline::highlight::Highlighter for WaferHelper {}
impl rustyline::validate::Validator for WaferHelper {}
impl rustyline::Helper for WaferHelper {}
/// History file: `$WAFER_HISTORY`, else `$XDG_STATE_HOME/wafer/history`,
/// else `~/.local/state/wafer/history`.
fn history_path() -> Option<std::path::PathBuf> {
if let Some(p) = std::env::var_os("WAFER_HISTORY") {
return Some(p.into());
}
let base = std::env::var_os("XDG_STATE_HOME")
.map(std::path::PathBuf::from)
.or_else(|| {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/state"))
})?;
Some(base.join("wafer/history"))
}
/// Interactive REPL: line editing, persistent history with prefix search
/// on Up/Down, and Tab completion over the live dictionary.
fn run_repl(vm: &mut ForthVM<NativeRuntime>) -> anyhow::Result<()> {
use rustyline::{Cmd, Editor, EventHandler, KeyCode, KeyEvent, Modifiers};
println!(
"WAFER v{} - WebAssembly Forth Engine in Rust",
env!("CARGO_PKG_VERSION")
);
println!("Type BYE to exit.");
let config = rustyline::Config::builder()
.completion_type(rustyline::CompletionType::List)
.history_ignore_dups(true)?
.build();
let mut rl: Editor<WaferHelper, rustyline::history::DefaultHistory> =
Editor::with_config(config)?;
rl.set_helper(Some(WaferHelper {
words: vm.word_names(),
}));
// Up/Down recall only entries starting with the typed prefix
rl.bind_sequence(
KeyEvent(KeyCode::Up, Modifiers::NONE),
EventHandler::Simple(Cmd::HistorySearchBackward),
);
rl.bind_sequence(
KeyEvent(KeyCode::Down, Modifiers::NONE),
EventHandler::Simple(Cmd::HistorySearchForward),
);
let history = history_path();
if let Some(path) = &history {
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = rl.load_history(path);
}
let save_history = |rl: &mut Editor<WaferHelper, rustyline::history::DefaultHistory>| {
if let Some(path) = &history {
let _ = rl.save_history(path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
}
};
loop {
let prompt = if vm.is_compiling() { " ] " } else { "> " };
match rl.readline(prompt) {
Ok(line) => {
let _ = rl.add_history_entry(&line);
match vm.evaluate(&line) {
Ok(()) => {
let output = vm.take_output();
if vm.bye_requested() {
break;
}
// PAGE (form feed) clears the terminal
if output.contains('\x0C') {
print!("\x1b[2J\x1b[H");
}
let output = output.replace('\x0C', "");
if !vm.is_compiling() {
if output.contains('\n') {
// Multi-line output (DUMP, WORDS, ...):
// print as a block, then ok on its own line
print!("{output}");
if !output.ends_with('\n') {
println!();
}
println!(" ok");
} else {
// Move cursor back up to end of input line so
// output appears inline, like traditional Forth:
// > 2 2 + . 4 ok
let col = prompt.len() + line.len() + 1;
print!("\x1b[A\x1b[{col}G {output} ok");
println!();
}
} else if !output.is_empty() {
print!("{output}");
}
}
Err(e) => {
eprintln!("Error: {e:#}");
}
}
// New definitions may have appeared: refresh completion
if let Some(h) = rl.helper_mut() {
h.words = vm.word_names();
}
save_history(&mut rl);
}
// Ctrl-C abandons the current line, Ctrl-D exits
Err(rustyline::error::ReadlineError::Interrupted) => {}
Err(rustyline::error::ReadlineError::Eof) => break,
Err(e) => {
eprintln!("Readline error: {e}");
break;
}
}
}
save_history(&mut rl);
Ok(())
}
+2 -33
View File
@@ -72,19 +72,6 @@
1- 1-
REPEAT ; REPEAT ;
\ ---------------------------------------------------------------
\ Common extensions (not in Forth 2012, gforth-compatible)
\ ---------------------------------------------------------------
\ -ROT ( x1 x2 x3 -- x3 x1 x2 ) rotate top item to third place
: -ROT ROT ROT ;
\ <= ( n1 n2 -- flag ) true if n1 <= n2 (signed)
: <= > 0= ;
\ >= ( n1 n2 -- flag ) true if n1 >= n2 (signed)
: >= < 0= ;
\ --------------------------------------------------------------- \ ---------------------------------------------------------------
\ Phase 2: Double-cell arithmetic \ Phase 2: Double-cell arithmetic
\ --------------------------------------------------------------- \ ---------------------------------------------------------------
@@ -197,8 +184,8 @@
\ TYPE ( c-addr u -- ) output u characters \ TYPE ( c-addr u -- ) output u characters
: TYPE 0 ?DO DUP C@ EMIT 1+ LOOP DROP ; : TYPE 0 ?DO DUP C@ EMIT 1+ LOOP DROP ;
\ SPACES ( n -- ) output n spaces (nothing for n <= 0, per 6.1.2230) \ SPACES ( n -- ) output n spaces
: SPACES 0 MAX 0 ?DO SPACE LOOP ; : SPACES 0 ?DO SPACE LOOP ;
\ Pictured numeric output constants \ Pictured numeric output constants
\ PICT_BUF_TOP = 0x05C0 = 1472, SYSVAR_HLD = 28 \ PICT_BUF_TOP = 0x05C0 = 1472, SYSVAR_HLD = 28
@@ -243,9 +230,6 @@
\ U. ( u -- ) print unsigned number and space \ U. ( u -- ) print unsigned number and space
: U. 0 <# #S #> TYPE SPACE ; : U. 0 <# #S #> TYPE SPACE ;
\ ? ( a-addr -- ) fetch and print
: ? @ . ;
\ .R ( n width -- ) print right-justified signed number \ .R ( n width -- ) print right-justified signed number
: .R >R DUP ABS 0 <# #S ROT SIGN #> R> OVER - SPACES TYPE ; : .R >R DUP ABS 0 <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
@@ -258,21 +242,6 @@
\ D.R ( d width -- ) print right-justified signed double \ D.R ( d width -- ) print right-justified signed double
: D.R >R SWAP OVER DABS <# #S ROT SIGN #> R> OVER - SPACES TYPE ; : D.R >R SWAP OVER DABS <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
\ ---------------------------------------------------------------
\ Return-stack introspection (debug aids)
\ ---------------------------------------------------------------
\ RDEPTH ( -- n ) number of cells on the return stack
\ RETURN_STACK_TOP = 9728 (0x2600). Only >R temps and loop params
\ live there; return addresses are on the WASM call stack.
: RDEPTH 9728 RP@ - 2 RSHIFT ;
\ .RS ( -- ) print the return stack bottom-to-top, like .S
\ Walks with BEGIN/WHILE (not DO) so the walk itself never pushes
\ onto the return stack it is printing.
: .RS ." R:<" RDEPTH 0 .R ." > "
9728 BEGIN DUP RP@ > WHILE 4 - DUP @ . REPEAT DROP ;
\ --------------------------------------------------------------- \ ---------------------------------------------------------------
\ Phase 6: DEFER support \ Phase 6: DEFER support
\ --------------------------------------------------------------- \ ---------------------------------------------------------------
+17 -162
View File
@@ -19,10 +19,7 @@ use wasm_encoder::{
use crate::dictionary::WordId; use crate::dictionary::WordId;
use crate::error::{WaferError, WaferResult}; use crate::error::{WaferError, WaferResult};
use crate::ir::IrOp; use crate::ir::IrOp;
use crate::memory::{ use crate::memory::{CELL_SIZE, SYSVAR_LEAVE_FLAG};
CELL_SIZE, DATA_STACK_BASE, DATA_STACK_TOP, FLOAT_STACK_BASE, FLOAT_STACK_TOP,
RETURN_STACK_BASE, RETURN_STACK_TOP, SYSVAR_FAULT_CODE, SYSVAR_LEAVE_FLAG,
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Import indices (order matters: imports numbered sequentially by kind) // Import indices (order matters: imports numbered sequentially by kind)
@@ -97,9 +94,6 @@ pub struct CodegenConfig {
pub table_size: u32, pub table_size: u32,
/// Enable stack-to-local promotion for straight-line words. /// Enable stack-to-local promotion for straight-line words.
pub stack_to_local_promotion: bool, pub stack_to_local_promotion: bool,
/// Table index of the `_STACK_FAULT_` host word; `Some` enables
/// stack under/overflow guards in the emitted code.
pub stack_guards: Option<u32>,
} }
/// Result of compiling a word to WASM. /// Result of compiling a word to WASM.
@@ -115,73 +109,16 @@ pub struct CompiledModule {
// Instruction-level helpers (free functions that take &mut Function) // Instruction-level helpers (free functions that take &mut Function)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stack-guard emission. The fault word's table index is stashed in a /// Decrement the cached `$dsp` local by `CELL_SIZE`.
// thread-local by `compile_word` (None = guards off) so the low-level
// push/pop helpers can stay plain `&mut Function` free functions
// without threading config through every emitter.
thread_local! {
static GUARD_FAULT: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
}
/// With guards on: emit `if <cond> { mem[SYSVAR_FAULT_CODE] = code;
/// call _STACK_FAULT_ }`. `cond` must leave an i32 boolean on the
/// operand stack. The fault host word throws, so the `if` never falls
/// through on the failure path.
fn emit_guard(f: &mut Function, code: i32, cond: impl FnOnce(&mut Function)) {
let Some(fault_idx) = GUARD_FAULT.get() else {
return;
};
cond(f);
f.instruction(&Instruction::If(BlockType::Empty))
.instruction(&Instruction::I32Const(SYSVAR_FAULT_CODE as i32))
.instruction(&Instruction::I32Const(code))
.instruction(&Instruction::I32Store(MEM4))
.instruction(&Instruction::I32Const(fault_idx as i32))
.instruction(&Instruction::CallIndirect {
type_index: TYPE_VOID,
table_index: TABLE,
})
.instruction(&Instruction::End);
}
/// Guard: data stack has at least `n` cells (else throw -4).
fn guard_dsp_underflow(f: &mut Function, n: u32) {
emit_guard(f, -4, |f| {
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Const((n * CELL_SIZE) as i32))
.instruction(&Instruction::I32Add)
.instruction(&Instruction::I32Const(DATA_STACK_TOP as i32))
.instruction(&Instruction::I32GtU);
});
}
/// Guard: data stack has room for `n` more cells (else throw -3).
fn guard_dsp_overflow(f: &mut Function, n: u32) {
emit_guard(f, -3, |f| {
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Const(
(DATA_STACK_BASE + n * CELL_SIZE) as i32,
))
.instruction(&Instruction::I32LtU);
});
}
/// Decrement the cached `$dsp` local by `CELL_SIZE` (allocate one cell).
/// This is the single choke point for data-stack pushes, so the
/// overflow guard lives here.
fn dsp_dec(f: &mut Function) { fn dsp_dec(f: &mut Function) {
guard_dsp_overflow(f, 1);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Const(CELL_SIZE as i32))
.instruction(&Instruction::I32Sub) .instruction(&Instruction::I32Sub)
.instruction(&Instruction::LocalSet(CACHED_DSP_LOCAL)); .instruction(&Instruction::LocalSet(CACHED_DSP_LOCAL));
} }
/// Increment the cached `$dsp` local by `CELL_SIZE` (free one cell). /// Increment the cached `$dsp` local by `CELL_SIZE`.
/// Single choke point for data-stack pops (`DROP` never loads the
/// value, so the underflow guard must sit here, not in `pop`).
fn dsp_inc(f: &mut Function) { fn dsp_inc(f: &mut Function) {
guard_dsp_underflow(f, 1);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Const(CELL_SIZE as i32))
.instruction(&Instruction::I32Add) .instruction(&Instruction::I32Add)
@@ -223,7 +160,6 @@ fn pop_to(f: &mut Function, local: u32) {
/// Read the top of the data stack without popping (value on operand stack). /// Read the top of the data stack without popping (value on operand stack).
fn peek(f: &mut Function) { fn peek(f: &mut Function) {
guard_dsp_underflow(f, 1);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Load(MEM4)); .instruction(&Instruction::I32Load(MEM4));
} }
@@ -246,13 +182,6 @@ fn dsp_reload(f: &mut Function) {
/// Push a value from the WASM operand stack onto the return stack via `tmp`. /// Push a value from the WASM operand stack onto the return stack via `tmp`.
fn rpush_via_local(f: &mut Function, tmp: u32) { fn rpush_via_local(f: &mut Function, tmp: u32) {
emit_guard(f, -5, |f| {
f.instruction(&Instruction::GlobalGet(RSP))
.instruction(&Instruction::I32Const(
(RETURN_STACK_BASE + CELL_SIZE) as i32,
))
.instruction(&Instruction::I32LtU);
});
f.instruction(&Instruction::LocalSet(tmp)); f.instruction(&Instruction::LocalSet(tmp));
// rsp -= CELL_SIZE // rsp -= CELL_SIZE
f.instruction(&Instruction::GlobalGet(RSP)) f.instruction(&Instruction::GlobalGet(RSP))
@@ -265,18 +194,8 @@ fn rpush_via_local(f: &mut Function, tmp: u32) {
.instruction(&Instruction::I32Store(MEM4)); .instruction(&Instruction::I32Store(MEM4));
} }
/// Guard: return stack is non-empty (else throw -6).
fn guard_rsp_underflow(f: &mut Function) {
emit_guard(f, -6, |f| {
f.instruction(&Instruction::GlobalGet(RSP))
.instruction(&Instruction::I32Const(RETURN_STACK_TOP as i32))
.instruction(&Instruction::I32GeU);
});
}
/// Pop the return stack onto the WASM operand stack. /// Pop the return stack onto the WASM operand stack.
fn rpop(f: &mut Function) { fn rpop(f: &mut Function) {
guard_rsp_underflow(f);
f.instruction(&Instruction::GlobalGet(RSP)) f.instruction(&Instruction::GlobalGet(RSP))
.instruction(&Instruction::I32Load(MEM4)); .instruction(&Instruction::I32Load(MEM4));
// rsp += CELL_SIZE // rsp += CELL_SIZE
@@ -288,7 +207,6 @@ fn rpop(f: &mut Function) {
/// Peek at the top of the return stack (no pop). /// Peek at the top of the return stack (no pop).
fn rpeek(f: &mut Function) { fn rpeek(f: &mut Function) {
guard_rsp_underflow(f);
f.instruction(&Instruction::GlobalGet(RSP)) f.instruction(&Instruction::GlobalGet(RSP))
.instruction(&Instruction::I32Load(MEM4)); .instruction(&Instruction::I32Load(MEM4));
} }
@@ -335,9 +253,7 @@ struct EmitCtx {
} }
/// Decrement the FSP global by 8 (allocate space for one f64). /// Decrement the FSP global by 8 (allocate space for one f64).
/// Single choke point for float pushes: overflow guard lives here.
fn fsp_dec(f: &mut Function) { fn fsp_dec(f: &mut Function) {
guard_fsp_overflow(f);
f.instruction(&Instruction::GlobalGet(FSP)) f.instruction(&Instruction::GlobalGet(FSP))
.instruction(&Instruction::I32Const(8)) .instruction(&Instruction::I32Const(8))
.instruction(&Instruction::I32Sub) .instruction(&Instruction::I32Sub)
@@ -345,10 +261,7 @@ fn fsp_dec(f: &mut Function) {
} }
/// Increment the FSP global by 8 (free space for one f64). /// Increment the FSP global by 8 (free space for one f64).
/// Single choke point for float pops (`FDROP` never loads the value):
/// underflow guard lives here.
fn fsp_inc(f: &mut Function) { fn fsp_inc(f: &mut Function) {
guard_fsp_underflow(f);
f.instruction(&Instruction::GlobalGet(FSP)) f.instruction(&Instruction::GlobalGet(FSP))
.instruction(&Instruction::I32Const(8)) .instruction(&Instruction::I32Const(8))
.instruction(&Instruction::I32Add) .instruction(&Instruction::I32Add)
@@ -365,24 +278,6 @@ fn fpush_via_local(f: &mut Function, tmp: u32) {
.instruction(&Instruction::F64Store(MEM8)); .instruction(&Instruction::F64Store(MEM8));
} }
/// Guard: float stack has room for one more f64 (else throw -44).
fn guard_fsp_overflow(f: &mut Function) {
emit_guard(f, -44, |f| {
f.instruction(&Instruction::GlobalGet(FSP))
.instruction(&Instruction::I32Const((FLOAT_STACK_BASE + 8) as i32))
.instruction(&Instruction::I32LtU);
});
}
/// Guard: float stack is non-empty (else throw -45).
fn guard_fsp_underflow(f: &mut Function) {
emit_guard(f, -45, |f| {
f.instruction(&Instruction::GlobalGet(FSP))
.instruction(&Instruction::I32Const(FLOAT_STACK_TOP as i32))
.instruction(&Instruction::I32GeU);
});
}
/// Decrement FSP, then store the f64 from local `src` at [FSP]. /// Decrement FSP, then store the f64 from local `src` at [FSP].
fn fpush_from_local(f: &mut Function, src: u32) { fn fpush_from_local(f: &mut Function, src: u32) {
fsp_dec(f); fsp_dec(f);
@@ -400,7 +295,6 @@ fn fpop(f: &mut Function) {
/// Load f64 from [FSP] onto the WASM operand stack without popping. /// Load f64 from [FSP] onto the WASM operand stack without popping.
fn fpeek(f: &mut Function) { fn fpeek(f: &mut Function) {
guard_fsp_underflow(f);
f.instruction(&Instruction::GlobalGet(FSP)) f.instruction(&Instruction::GlobalGet(FSP))
.instruction(&Instruction::F64Load(MEM8)); .instruction(&Instruction::F64Load(MEM8));
} }
@@ -899,20 +793,9 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) {
.instruction(&Instruction::I32Store(MEM4)); .instruction(&Instruction::I32Store(MEM4));
} }
IrOp::RpFetch => {
// Push the current return-stack pointer onto the data stack.
// `$rsp` lives in a global (not cached), so no writeback needed.
dsp_dec(f);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::GlobalGet(RSP))
.instruction(&Instruction::I32Store(MEM4));
}
// -- Compound operations ----------------------------------------------- // -- Compound operations -----------------------------------------------
IrOp::TwoDup => { IrOp::TwoDup => {
// ( a b -- a b a b ) // ( a b -- a b a b )
guard_dsp_underflow(f, 2);
guard_dsp_overflow(f, 2);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Load(MEM4)); // b .instruction(&Instruction::I32Load(MEM4)); // b
f.instruction(&Instruction::LocalSet(SCRATCH_BASE)); f.instruction(&Instruction::LocalSet(SCRATCH_BASE));
@@ -939,7 +822,6 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) {
IrOp::TwoDrop => { IrOp::TwoDrop => {
// ( a b -- ) // ( a b -- )
guard_dsp_underflow(f, 2);
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
.instruction(&Instruction::I32Const((CELL_SIZE * 2) as i32)) .instruction(&Instruction::I32Const((CELL_SIZE * 2) as i32))
.instruction(&Instruction::I32Add) .instruction(&Instruction::I32Add)
@@ -1206,10 +1088,12 @@ fn emit_do_loop(f: &mut Function, body: &[IrOp], is_plus_loop: bool, ctx: &mut E
.instruction(&Instruction::End); .instruction(&Instruction::End);
} }
// if index >= limit, exit // Forth 2012: LOOP exits when the index crosses the boundary between
// limit-1 and limit. With step +1 that is exactly new_index == limit
// in wraparound arithmetic — start >= limit must wrap, not exit early.
f.instruction(&Instruction::LocalGet(index_local)) f.instruction(&Instruction::LocalGet(index_local))
.instruction(&Instruction::LocalGet(limit_local)) .instruction(&Instruction::LocalGet(limit_local))
.instruction(&Instruction::I32GeS) .instruction(&Instruction::I32Eq)
.instruction(&Instruction::BrIf(1)) // break to $exit .instruction(&Instruction::BrIf(1)) // break to $exit
.instruction(&Instruction::Br(0)) // continue loop .instruction(&Instruction::Br(0)) // continue loop
.instruction(&Instruction::End) // end loop .instruction(&Instruction::End) // end loop
@@ -1251,9 +1135,7 @@ fn is_promotable(ops: &[IrOp]) -> bool {
fn is_promotable_body(ops: &[IrOp]) -> bool { fn is_promotable_body(ops: &[IrOp]) -> bool {
for op in ops { for op in ops {
match op { match op {
IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute | IrOp::SpFetch | IrOp::RpFetch => { IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute | IrOp::SpFetch => return false,
return false;
}
IrOp::ToR | IrOp::FromR | IrOp::Exit => return false, IrOp::ToR | IrOp::FromR | IrOp::Exit => return false,
IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return false, IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return false,
IrOp::ForthFLocalGet(_) | IrOp::ForthFLocalSet(_) => return false, IrOp::ForthFLocalGet(_) | IrOp::ForthFLocalSet(_) => return false,
@@ -1665,11 +1547,6 @@ impl StackSim {
/// Emit the promoted prologue: load `preload` items from the memory stack /// Emit the promoted prologue: load `preload` items from the memory stack
/// into WASM locals. /// into WASM locals.
fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) { fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) {
// One entry check covers the whole promoted word: the caller must
// have at least `preload` cells on the data stack.
if preload > 0 {
guard_dsp_underflow(f, preload);
}
// Load items: mem[dsp] = top of stack, mem[dsp+4] = second, etc. // Load items: mem[dsp] = top of stack, mem[dsp+4] = second, etc.
// We load them top-first, then reverse the sim stack so that // We load them top-first, then reverse the sim stack so that
// sim.stack[0] = deepest loaded, sim.stack[last] = top. // sim.stack[0] = deepest loaded, sim.stack[last] = top.
@@ -1700,7 +1577,6 @@ fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) {
fn emit_promoted_epilogue(f: &mut Function, sim: &mut StackSim) { fn emit_promoted_epilogue(f: &mut Function, sim: &mut StackSim) {
let remaining = sim.stack.len() as u32; let remaining = sim.stack.len() as u32;
if remaining > 0 { if remaining > 0 {
guard_dsp_overflow(f, remaining);
// Decrement cached DSP for the items we're pushing back // Decrement cached DSP for the items we're pushing back
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL));
f.instruction(&Instruction::I32Const((remaining * CELL_SIZE) as i32)); f.instruction(&Instruction::I32Const((remaining * CELL_SIZE) as i32));
@@ -2023,7 +1899,8 @@ fn emit_promoted_op(f: &mut Function, op: &IrOp, sim: &mut StackSim) {
// Fix up stack for next iteration (LOOP body is stack-neutral) // Fix up stack for next iteration (LOOP body is stack-neutral)
emit_promoted_loop_fixup(f, sim, &loop_top_stack); emit_promoted_loop_fixup(f, sim, &loop_top_stack);
// LOOP: increment by 1, check >= limit // LOOP: increment by 1, exit when new_index == limit
// (Forth 2012 boundary crossing; start >= limit wraps around)
f.instruction(&Instruction::LocalGet(index_local)); f.instruction(&Instruction::LocalGet(index_local));
f.instruction(&Instruction::I32Const(1)); f.instruction(&Instruction::I32Const(1));
f.instruction(&Instruction::I32Add); f.instruction(&Instruction::I32Add);
@@ -2031,7 +1908,7 @@ fn emit_promoted_op(f: &mut Function, op: &IrOp, sim: &mut StackSim) {
f.instruction(&Instruction::LocalGet(index_local)); f.instruction(&Instruction::LocalGet(index_local));
f.instruction(&Instruction::LocalGet(limit_local)); f.instruction(&Instruction::LocalGet(limit_local));
f.instruction(&Instruction::I32GeS); f.instruction(&Instruction::I32Eq);
f.instruction(&Instruction::BrIf(1)); // break to $exit f.instruction(&Instruction::BrIf(1)); // break to $exit
} }
@@ -2317,9 +2194,6 @@ fn body_needs_return_stack(ops: &[IrOp]) -> bool {
match op { match op {
IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute => return true, IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute => return true,
IrOp::ToR | IrOp::FromR => return true, IrOp::ToR | IrOp::FromR => return true,
// RP@ observes the return stack, so loop params must be there
// (otherwise inlined RDEPTH/.RS would report an empty stack).
IrOp::RpFetch => return true,
// RFetch (I) is handled by loop locals in the fast path — not a problem. // RFetch (I) is handled by loop locals in the fast path — not a problem.
// LoopJ is also handled by loop locals. // LoopJ is also handled by loop locals.
// Only explicit >R / R> / calls force the slow path. // Only explicit >R / R> / calls force the slow path.
@@ -2532,13 +2406,10 @@ fn count_forth_f_locals(ops: &[IrOp]) -> u32 {
/// This is the JIT path: each word gets its own module that imports /// This is the JIT path: each word gets its own module that imports
/// shared memory, globals, and function table from the host. /// shared memory, globals, and function table from the host.
pub fn compile_word( pub fn compile_word(
name: &str, _name: &str,
body: &[IrOp], body: &[IrOp],
config: &CodegenConfig, config: &CodegenConfig,
) -> WaferResult<CompiledModule> { ) -> WaferResult<CompiledModule> {
// Arm (or disarm) stack-guard emission for this compilation.
GUARD_FAULT.set(config.stack_guards);
let mut module = Module::new(); let mut module = Module::new();
// -- Type section -- // -- Type section --
@@ -2699,16 +2570,6 @@ pub fn compile_word(
code.function(&func); code.function(&func);
module.section(&code); module.section(&code);
// -- Name section: carries the Forth word name into wasmtime trap
// backtraces (best-effort symbolication, WS-008).
let mut names = wasm_encoder::NameSection::new();
names.module(name);
let mut fn_names = wasm_encoder::NameMap::new();
fn_names.append(0, "emit");
fn_names.append(WORD_FUNC, name);
names.functions(&fn_names);
module.section(&names);
let bytes = module.finish(); let bytes = module.finish();
// Validate // Validate
@@ -2971,9 +2832,11 @@ fn emit_consolidated_do_loop(
.instruction(&Instruction::End); .instruction(&Instruction::End);
} }
// Forth 2012 boundary crossing: exit when new_index == limit
// (start >= limit wraps around instead of exiting early).
f.instruction(&Instruction::LocalGet(index_local)) f.instruction(&Instruction::LocalGet(index_local))
.instruction(&Instruction::LocalGet(limit_local)) .instruction(&Instruction::LocalGet(limit_local))
.instruction(&Instruction::I32GeS) .instruction(&Instruction::I32Eq)
.instruction(&Instruction::BrIf(1)) .instruction(&Instruction::BrIf(1))
.instruction(&Instruction::Br(0)) .instruction(&Instruction::Br(0))
.instruction(&Instruction::End) .instruction(&Instruction::End)
@@ -3013,9 +2876,8 @@ pub fn compile_consolidated_module(
words: &[(WordId, Vec<IrOp>)], words: &[(WordId, Vec<IrOp>)],
local_fn_map: &HashMap<WordId, u32>, local_fn_map: &HashMap<WordId, u32>,
table_size: u32, table_size: u32,
stack_guards: Option<u32>,
) -> WaferResult<Vec<u8>> { ) -> WaferResult<Vec<u8>> {
compile_multi_word_module(words, local_fn_map, table_size, None, stack_guards) compile_multi_word_module(words, local_fn_map, table_size, None)
} }
/// Compile an exportable WASM module with embedded memory and metadata. /// Compile an exportable WASM module with embedded memory and metadata.
@@ -3028,9 +2890,8 @@ pub fn compile_exportable_module(
local_fn_map: &HashMap<WordId, u32>, local_fn_map: &HashMap<WordId, u32>,
table_size: u32, table_size: u32,
export: &ExportSections<'_>, export: &ExportSections<'_>,
stack_guards: Option<u32>,
) -> WaferResult<Vec<u8>> { ) -> WaferResult<Vec<u8>> {
compile_multi_word_module(words, local_fn_map, table_size, Some(export), stack_guards) compile_multi_word_module(words, local_fn_map, table_size, Some(export))
} }
/// Internal: build a multi-word WASM module. When `export` is `Some`, adds /// Internal: build a multi-word WASM module. When `export` is `Some`, adds
@@ -3040,11 +2901,7 @@ fn compile_multi_word_module(
local_fn_map: &HashMap<WordId, u32>, local_fn_map: &HashMap<WordId, u32>,
table_size: u32, table_size: u32,
export: Option<&ExportSections<'_>>, export: Option<&ExportSections<'_>>,
stack_guards: Option<u32>,
) -> WaferResult<Vec<u8>> { ) -> WaferResult<Vec<u8>> {
// Arm (or disarm) stack-guard emission for this module.
GUARD_FAULT.set(stack_guards);
let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty()); let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty());
let mut module = Module::new(); let mut module = Module::new();
@@ -3273,7 +3130,6 @@ mod tests {
base_fn_index: 0, base_fn_index: 0,
table_size: 16, table_size: 16,
stack_to_local_promotion: true, stack_to_local_promotion: true,
stack_guards: None,
} }
} }
@@ -3498,7 +3354,6 @@ mod tests {
base_fn_index: 7, base_fn_index: 7,
table_size: 16, table_size: 16,
stack_to_local_promotion: true, stack_to_local_promotion: true,
stack_guards: None,
}; };
let m = compile_word("t", &[IrOp::PushI32(1)], &cfg).unwrap(); let m = compile_word("t", &[IrOp::PushI32(1)], &cfg).unwrap();
assert_eq!(m.fn_index, 7); assert_eq!(m.fn_index, 7);
-7
View File
@@ -7,11 +7,6 @@ use crate::optimizer::OptConfig;
pub struct CodegenOpts { pub struct CodegenOpts {
/// Enable stack-to-local promotion for straight-line words. /// Enable stack-to-local promotion for straight-line words.
pub stack_to_local_promotion: bool, pub stack_to_local_promotion: bool,
/// Emit stack under/overflow guards in compiled words. Faults throw
/// standard codes (-3/-4/-5/-6/-44/-45) instead of silently
/// corrupting stack pointers. On by default; benchmarks and
/// exported production modules turn it off.
pub stack_guards: bool,
} }
/// Master configuration for all WAFER optimizations. /// Master configuration for all WAFER optimizations.
@@ -37,7 +32,6 @@ impl WaferConfig {
}, },
codegen: CodegenOpts { codegen: CodegenOpts {
stack_to_local_promotion: true, stack_to_local_promotion: true,
stack_guards: true,
}, },
} }
} }
@@ -55,7 +49,6 @@ impl WaferConfig {
}, },
codegen: CodegenOpts { codegen: CodegenOpts {
stack_to_local_promotion: false, stack_to_local_promotion: false,
stack_guards: false,
}, },
} }
} }
+9 -9
View File
@@ -21,7 +21,7 @@ mod tests {
// Empty word list should produce nothing (but we guard against this at call site) // Empty word list should produce nothing (but we guard against this at call site)
let words = vec![]; let words = vec![];
let map = HashMap::new(); let map = HashMap::new();
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
// Empty is valid -- should produce a valid module with no functions // Empty is valid -- should produce a valid module with no functions
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -31,7 +31,7 @@ mod tests {
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])]; let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); // function index 1 (after emit import) map.insert(WordId(1), 1u32); // function index 1 (after emit import)
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -49,7 +49,7 @@ mod tests {
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
map.insert(WordId(3), 3u32); map.insert(WordId(3), 3u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -59,7 +59,7 @@ mod tests {
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])]; let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(3), 1u32); map.insert(WordId(3), 1u32);
let result = compile_consolidated_module(&words, &map, 256, None); let result = compile_consolidated_module(&words, &map, 256);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -72,7 +72,7 @@ mod tests {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -95,7 +95,7 @@ mod tests {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -120,7 +120,7 @@ mod tests {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -141,7 +141,7 @@ mod tests {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -163,7 +163,7 @@ mod tests {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(WordId(1), 1u32); map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32); map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None); let result = compile_consolidated_module(&words, &map, 16);
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }
+9 -34
View File
@@ -18,8 +18,6 @@ pub mod flags {
pub const IMMEDIATE: u8 = 0x80; pub const IMMEDIATE: u8 = 0x80;
/// Word is hidden (being compiled, not yet findable). /// Word is hidden (being compiled, not yet findable).
pub const HIDDEN: u8 = 0x40; pub const HIDDEN: u8 = 0x40;
/// Word is an implementation detail: findable, but skipped by WORDS.
pub const INTERNAL: u8 = 0x20;
/// Mask for the name length (lower 5 bits). /// Mask for the name length (lower 5 bits).
pub const LENGTH_MASK: u8 = 0x1F; pub const LENGTH_MASK: u8 = 0x1F;
/// Maximum word name length. /// Maximum word name length.
@@ -97,17 +95,11 @@ impl Dictionary {
// Write link field (points to previous LATEST) // Write link field (points to previous LATEST)
self.write_u32_unchecked(entry_start, self.latest); self.write_u32_unchecked(entry_start, self.latest);
// Write flags byte: HIDDEN | length, optionally IMMEDIATE. // Write flags byte: HIDDEN | length, optionally IMMEDIATE
// Underscore-prefixed names are implementation details by repo
// convention (see tools/editor-support): flag them INTERNAL so
// WORDS and completion skip them while FIND still works.
let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK); let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK);
if immediate { if immediate {
flag_byte |= flags::IMMEDIATE; flag_byte |= flags::IMMEDIATE;
} }
if name_bytes.first() == Some(&b'_') {
flag_byte |= flags::INTERNAL;
}
self.memory[(entry_start + 4) as usize] = flag_byte; self.memory[(entry_start + 4) as usize] = flag_byte;
// Write name bytes // Write name bytes
@@ -192,9 +184,10 @@ impl Dictionary {
} }
} }
} }
// In no wordlist of the search order: not findable // Fallback: return newest entry across all wordlists
// (Forth 2012 §16.3.3 — the order is authoritative). if let Some(&(_wid, word_addr, fn_index, is_immediate)) = entries.last() {
return None; return Some((word_addr, WordId(fn_index), is_immediate));
}
} }
// Fallback: linked-list walk (for words not yet in the index) // Fallback: linked-list walk (for words not yet in the index)
@@ -417,21 +410,8 @@ impl Dictionary {
} }
/// Return names of all visible (non-hidden) words, newest first. /// Return names of all visible (non-hidden) words, newest first.
/// With `include_internal` false, words flagged INTERNAL are skipped. pub fn visible_words(&self) -> Vec<String> {
pub fn visible_words(&self, include_internal: bool) -> Vec<String> { let mut names = Vec::new();
self.visible_entries()
.into_iter()
.filter(|(_, _, internal)| include_internal || !internal)
.map(|(name, _, _)| name)
.collect()
}
/// All visible (non-hidden) entries, newest first:
/// (name, wordlist id, INTERNAL flag). The wid comes from the hash
/// index (entries themselves store no wid); words missing from the
/// index default to wid 1 (FORTH).
pub fn visible_entries(&self) -> Vec<(String, u32, bool)> {
let mut entries = Vec::new();
let mut addr = self.latest; let mut addr = self.latest;
while addr != 0 { while addr != 0 {
let flags_byte = self.memory[(addr + 4) as usize]; let flags_byte = self.memory[(addr + 4) as usize];
@@ -440,12 +420,7 @@ impl Dictionary {
let name_start = (addr + 5) as usize; let name_start = (addr + 5) as usize;
let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len]) let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len])
.to_string(); .to_string();
let wid = self names.push(name);
.index
.get(&name)
.and_then(|es| es.iter().find(|e| e.1 == addr))
.map_or(1, |e| e.0);
entries.push((name, wid, flags_byte & flags::INTERNAL != 0));
} }
let link = self.read_u32_unchecked(addr); let link = self.read_u32_unchecked(addr);
if link == addr { if link == addr {
@@ -453,7 +428,7 @@ impl Dictionary {
} }
addr = link; addr = link;
} }
entries names
} }
/// Get a reference to the raw memory buffer. /// Get a reference to the raw memory buffer.
-7
View File
@@ -61,13 +61,6 @@ pub enum WaferError {
#[error("{0}")] #[error("{0}")]
Abort(String), Abort(String),
/// An uncaught Forth THROW as reported to the user. `message` is the
/// full display text (standard message or ABORT" payload); `code`
/// carries the THROW code for typed consumers (CLI exit paths, web
/// REPL styling) via `Error::downcast_ref`.
#[error("{message}")]
UncaughtThrow { code: i32, message: String },
} }
/// Result type alias for WAFER operations. /// Result type alias for WAFER operations.
+2 -8
View File
@@ -120,14 +120,8 @@ pub fn export_module(
metadata_json: metadata_json.as_bytes(), metadata_json: metadata_json.as_bytes(),
}; };
let wasm_bytes = compile_exportable_module( let wasm_bytes = compile_exportable_module(&words, &local_fn_map, table_size, &export_sections)
&words, .map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
&local_fn_map,
table_size,
&export_sections,
vm.stack_guard_param(),
)
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
Ok((wasm_bytes, metadata)) Ok((wasm_bytes, metadata))
} }
-2
View File
@@ -159,8 +159,6 @@ pub enum IrOp {
Execute, Execute,
/// Push the current data-stack pointer: ( -- addr ) /// Push the current data-stack pointer: ( -- addr )
SpFetch, SpFetch,
/// Push the current return-stack pointer: ( -- addr )
RpFetch,
// -- Float stack manipulation -- // -- Float stack manipulation --
/// Float duplicate: ( F: r -- r r ) /// Float duplicate: ( F: r -- r r )
-2
View File
@@ -24,8 +24,6 @@ pub mod ir;
pub mod memory; pub mod memory;
pub mod optimizer; pub mod optimizer;
pub mod runtime; pub mod runtime;
pub mod see;
pub mod wordhelp;
// Outer interpreter: runtime-agnostic, works with any Runtime impl // Outer interpreter: runtime-agnostic, works with any Runtime impl
#[allow(trivial_numeric_casts, clippy::unnecessary_cast)] #[allow(trivial_numeric_casts, clippy::unnecessary_cast)]
-22
View File
@@ -106,25 +106,6 @@ pub const SYSVAR_NUM_TIB: u32 = SYSVAR_BASE + 24;
pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28; pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
/// LEAVE flag: nonzero when LEAVE has been called inside a DO loop. /// LEAVE flag: nonzero when LEAVE has been called inside a DO loop.
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32; pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
/// Throw code left by a compiled stack-guard fault for `_STACK_FAULT_`.
pub const SYSVAR_FAULT_CODE: u32 = SYSVAR_BASE + 36;
/// DPL: digits right of the rightmost punctuation in the last converted
/// number; negative when the token carried no punctuation.
pub const SYSVAR_DPL: u32 = SYSVAR_BASE + 40;
/// NH: high-order cell of the last single-cell conversion, so an
/// out-of-range token can be recovered as a double.
pub const SYSVAR_NH: u32 = SYSVAR_BASE + 44;
/// Seed for [`SYSVAR_DPL`] before conversion starts.
///
/// `SwiftForth` seeds DPL with a negative value and bumps it once per digit,
/// so an unpunctuated token still ends up negative. Punctuation resets the
/// counter to zero, which makes the final value the digit count right of the
/// rightmost punctuation character.
///
/// The exact seed is observable: `sf64` reports DPL as -1020 after `1234`
/// and -1023 after `-1`, both of which pin it to -1024.
pub const DPL_INIT: i32 = -1024;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -166,9 +147,6 @@ mod tests {
SYSVAR_NUM_TIB, SYSVAR_NUM_TIB,
SYSVAR_HLD, SYSVAR_HLD,
SYSVAR_LEAVE_FLAG, SYSVAR_LEAVE_FLAG,
SYSVAR_FAULT_CODE,
SYSVAR_DPL,
SYSVAR_NH,
]; ];
for offset in all_offsets { for offset in all_offsets {
assert!(offset + CELL_SIZE <= SYSVAR_BASE + SYSVAR_SIZE); assert!(offset + CELL_SIZE <= SYSVAR_BASE + SYSVAR_SIZE);
+393 -2298
View File
File diff suppressed because it is too large Load Diff
+2 -21
View File
@@ -98,29 +98,11 @@ impl HostAccess for CallerHostAccess<'_, '_> {
let func = *func_ref let func = *func_ref
.unwrap_func() .unwrap_func()
.ok_or_else(|| anyhow::anyhow!("call_func: null funcref {fn_index}"))?; .ok_or_else(|| anyhow::anyhow!("call_func: null funcref {fn_index}"))?;
func.call(&mut *self.caller, &[], &mut []) func.call(&mut *self.caller, &[], &mut [])?;
.map_err(name_trap_frame)?;
Ok(()) Ok(())
} }
} }
/// Prefix a wasmtime trap error with the innermost named WASM frame.
/// Compiled words carry their Forth name in the module name section, so a
/// genuine trap reads "in <WORD>: wasm trap: ...". THROW-driven unwinds
/// also pass through here, but CATCH and `describe_uncaught` key on the
/// shared `throw_code` cell, never on the message, so the wrap is inert
/// for them.
fn name_trap_frame(e: wasmtime::Error) -> wasmtime::Error {
let name = e
.downcast_ref::<wasmtime::WasmBacktrace>()
.and_then(|bt| bt.frames().iter().find_map(|f| f.func_name()))
.map(str::to_string);
match name {
Some(n) => e.context(format!("in {n}")),
None => e,
}
}
/// Wasmtime-based native runtime. /// Wasmtime-based native runtime.
pub struct NativeRuntime { pub struct NativeRuntime {
engine: Engine, engine: Engine,
@@ -311,8 +293,7 @@ impl Runtime for NativeRuntime {
let func = *r let func = *r
.unwrap_func() .unwrap_func()
.ok_or_else(|| anyhow::anyhow!("word {fn_index} is null funcref"))?; .ok_or_else(|| anyhow::anyhow!("word {fn_index} is null funcref"))?;
func.call(&mut self.store, &[], &mut []) func.call(&mut self.store, &[], &mut [])?;
.map_err(name_trap_frame)?;
Ok(()) Ok(())
} }
-376
View File
@@ -1,376 +0,0 @@
//! IR pretty-printer for `SEE-IR` and the `SEE` fallback path.
//!
//! Renders a post-optimization IR body as indented, one-op-per-line text.
//! Simple ops print as short lowercase mnemonics (Forth glyphs where they
//! are universally recognizable: `@`, `!`, `0=`, `>r`, ...); structured ops
//! print as Forth control words with 2-space indented bodies. Calls resolve
//! `WordId`s to names through an optional resolver so the formatter itself
//! stays independent of the VM.
use crate::dictionary::WordId;
use crate::ir::IrOp;
/// Format an IR body as indented, one-op-per-line text.
pub fn format_ir(ops: &[IrOp]) -> String {
format_ir_with(ops, &|_| None)
}
/// Like [`format_ir`], resolving `Call`/`TailCall`/`Execute` targets to word
/// names via `resolve`; unresolved ids print as `#N`.
pub fn format_ir_with(ops: &[IrOp], resolve: &dyn Fn(WordId) -> Option<String>) -> String {
let mut out = String::new();
write_ops(&mut out, ops, 0, resolve);
out
}
fn line(out: &mut String, depth: usize, text: &str) {
for _ in 0..depth {
out.push_str(" ");
}
out.push_str(text);
out.push('\n');
}
fn callee(id: WordId, resolve: &dyn Fn(WordId) -> Option<String>) -> String {
resolve(id).unwrap_or_else(|| format!("#{}", id.0))
}
fn write_ops(
out: &mut String,
ops: &[IrOp],
depth: usize,
resolve: &dyn Fn(WordId) -> Option<String>,
) {
for op in ops {
write_op(out, op, depth, resolve);
}
}
fn write_op(out: &mut String, op: &IrOp, depth: usize, resolve: &dyn Fn(WordId) -> Option<String>) {
// Exhaustive on purpose: a new IrOp variant must show up here at
// compile time, not silently render wrong.
let simple: String = match op {
// -- Literals --
IrOp::PushI32(v) => format!("push {v}"),
IrOp::PushI64(v) => format!("push64 {v}"),
IrOp::PushF64(v) => format!("fpush {v}"),
// -- Stack manipulation --
IrOp::Drop => "drop".into(),
IrOp::Dup => "dup".into(),
IrOp::Swap => "swap".into(),
IrOp::Over => "over".into(),
IrOp::Rot => "rot".into(),
IrOp::Nip => "nip".into(),
IrOp::Tuck => "tuck".into(),
IrOp::TwoDup => "2dup".into(),
IrOp::TwoDrop => "2drop".into(),
// -- Arithmetic --
IrOp::Add => "add".into(),
IrOp::Sub => "sub".into(),
IrOp::Mul => "mul".into(),
IrOp::DivMod => "divmod".into(),
IrOp::Negate => "negate".into(),
IrOp::Abs => "abs".into(),
// -- Comparison --
IrOp::Eq => "eq".into(),
IrOp::NotEq => "ne".into(),
IrOp::Lt => "lt".into(),
IrOp::Gt => "gt".into(),
IrOp::LtUnsigned => "u<".into(),
IrOp::ZeroEq => "0=".into(),
IrOp::ZeroLt => "0<".into(),
// -- Logic --
IrOp::And => "and".into(),
IrOp::Or => "or".into(),
IrOp::Xor => "xor".into(),
IrOp::Invert => "invert".into(),
IrOp::Lshift => "lshift".into(),
IrOp::Rshift => "rshift".into(),
IrOp::ArithRshift => "arshift".into(),
// -- Memory --
IrOp::Fetch => "@".into(),
IrOp::Store => "!".into(),
IrOp::CFetch => "c@".into(),
IrOp::CStore => "c!".into(),
IrOp::PlusStore => "+!".into(),
// -- Calls --
IrOp::Call(id) => format!("call {}", callee(*id, resolve)),
IrOp::TailCall(id) => format!("tail-call {}", callee(*id, resolve)),
// -- Structured control flow (multi-line) --
IrOp::If {
then_body,
else_body,
} => {
line(out, depth, "if");
write_ops(out, then_body, depth + 1, resolve);
if let Some(eb) = else_body {
line(out, depth, "else");
write_ops(out, eb, depth + 1, resolve);
}
line(out, depth, "then");
return;
}
IrOp::DoLoop { body, is_plus_loop } => {
line(out, depth, "do");
write_ops(out, body, depth + 1, resolve);
line(out, depth, if *is_plus_loop { "+loop" } else { "loop" });
return;
}
IrOp::BeginUntil { body } => {
line(out, depth, "begin");
write_ops(out, body, depth + 1, resolve);
line(out, depth, "until");
return;
}
IrOp::BeginAgain { body } => {
line(out, depth, "begin");
write_ops(out, body, depth + 1, resolve);
line(out, depth, "again");
return;
}
IrOp::BeginWhileRepeat { test, body } => {
line(out, depth, "begin");
write_ops(out, test, depth + 1, resolve);
line(out, depth, "while");
write_ops(out, body, depth + 1, resolve);
line(out, depth, "repeat");
return;
}
IrOp::BeginDoubleWhileRepeat {
outer_test,
inner_test,
body,
after_repeat,
else_body,
} => {
line(out, depth, "begin");
write_ops(out, outer_test, depth + 1, resolve);
line(out, depth, "while");
write_ops(out, inner_test, depth + 1, resolve);
line(out, depth, "while");
write_ops(out, body, depth + 1, resolve);
line(out, depth, "repeat");
write_ops(out, after_repeat, depth + 1, resolve);
if let Some(eb) = else_body {
line(out, depth, "else");
write_ops(out, eb, depth + 1, resolve);
}
line(out, depth, "then");
return;
}
IrOp::Exit => "exit".into(),
IrOp::LoopRestartIfFalse => "loop-restart-if-false".into(),
// -- Flat forward branches --
IrOp::Block(l) => format!("block L{l}"),
IrOp::BranchIfFalse(l) => format!("branch-if-false L{l}"),
IrOp::EndBlock(l) => format!("end-block L{l}"),
// -- Return stack --
IrOp::ToR => ">r".into(),
IrOp::FromR => "r>".into(),
IrOp::RFetch => "r@".into(),
IrOp::LoopJ => "j".into(),
// -- Forth locals --
IrOp::ForthLocalGet(n) => format!("local@ {n}"),
IrOp::ForthLocalSet(n) => format!("local! {n}"),
IrOp::ForthFLocalGet(n) => format!("flocal@ {n}"),
IrOp::ForthFLocalSet(n) => format!("flocal! {n}"),
// -- I/O --
IrOp::Emit => "emit".into(),
IrOp::Dot => ".".into(),
IrOp::Cr => "cr".into(),
IrOp::Type => "type".into(),
// -- System --
IrOp::Execute => "execute".into(),
IrOp::SpFetch => "sp@".into(),
IrOp::RpFetch => "rp@".into(),
// -- Float stack --
IrOp::FDup => "fdup".into(),
IrOp::FDrop => "fdrop".into(),
IrOp::FSwap => "fswap".into(),
IrOp::FOver => "fover".into(),
// -- Float arithmetic --
IrOp::FAdd => "fadd".into(),
IrOp::FSub => "fsub".into(),
IrOp::FMul => "fmul".into(),
IrOp::FDiv => "fdiv".into(),
IrOp::FNegate => "fnegate".into(),
IrOp::FAbs => "fabs".into(),
IrOp::FSqrt => "fsqrt".into(),
IrOp::FMin => "fmin".into(),
IrOp::FMax => "fmax".into(),
IrOp::FFloor => "ffloor".into(),
IrOp::FRound => "fround".into(),
// -- Float comparisons --
IrOp::FZeroEq => "f0=".into(),
IrOp::FZeroLt => "f0<".into(),
IrOp::FEq => "f=".into(),
IrOp::FLt => "f<".into(),
// -- Float memory --
IrOp::FetchFloat => "f@".into(),
IrOp::StoreFloat => "f!".into(),
// -- Conversions --
IrOp::StoF => "s>f".into(),
IrOp::FtoS => "f>s".into(),
};
line(out, depth, &simple);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_ops_one_per_line() {
let out = format_ir(&[IrOp::Dup, IrOp::Mul, IrOp::PushI32(7)]);
assert_eq!(out, "dup\nmul\npush 7\n");
}
#[test]
fn call_resolves_via_resolver() {
let ops = [IrOp::Call(WordId(12)), IrOp::TailCall(WordId(13))];
assert_eq!(format_ir(&ops), "call #12\ntail-call #13\n");
let named = format_ir_with(&ops, &|id| (id.0 == 12).then(|| "SQ".to_string()));
assert_eq!(named, "call SQ\ntail-call #13\n");
}
#[test]
fn nested_if_inside_do_loop_indents() {
let ops = [IrOp::DoLoop {
body: vec![
IrOp::Dup,
IrOp::If {
then_body: vec![IrOp::Dup, IrOp::Mul],
else_body: Some(vec![IrOp::Drop]),
},
],
is_plus_loop: false,
}];
let expected = "do\n dup\n if\n dup\n mul\n else\n drop\n then\nloop\n";
assert_eq!(format_ir(&ops), expected);
}
#[test]
fn while_loops_and_flat_branches() {
let ops = [
IrOp::BeginWhileRepeat {
test: vec![IrOp::Dup],
body: vec![IrOp::PushI32(1), IrOp::Sub],
},
IrOp::Block(3),
IrOp::BranchIfFalse(3),
IrOp::EndBlock(3),
];
let expected = "begin\n dup\nwhile\n push 1\n sub\nrepeat\nblock L3\nbranch-if-false L3\nend-block L3\n";
assert_eq!(format_ir(&ops), expected);
}
#[test]
fn every_simple_variant_renders() {
// One of each non-structured op; count of output lines must match.
let ops = vec![
IrOp::PushI32(1),
IrOp::PushI64(2),
IrOp::PushF64(1.5),
IrOp::Drop,
IrOp::Dup,
IrOp::Swap,
IrOp::Over,
IrOp::Rot,
IrOp::Nip,
IrOp::Tuck,
IrOp::TwoDup,
IrOp::TwoDrop,
IrOp::Add,
IrOp::Sub,
IrOp::Mul,
IrOp::DivMod,
IrOp::Negate,
IrOp::Abs,
IrOp::Eq,
IrOp::NotEq,
IrOp::Lt,
IrOp::Gt,
IrOp::LtUnsigned,
IrOp::ZeroEq,
IrOp::ZeroLt,
IrOp::And,
IrOp::Or,
IrOp::Xor,
IrOp::Invert,
IrOp::Lshift,
IrOp::Rshift,
IrOp::ArithRshift,
IrOp::Fetch,
IrOp::Store,
IrOp::CFetch,
IrOp::CStore,
IrOp::PlusStore,
IrOp::Call(WordId(1)),
IrOp::TailCall(WordId(2)),
IrOp::Exit,
IrOp::LoopRestartIfFalse,
IrOp::Block(1),
IrOp::BranchIfFalse(1),
IrOp::EndBlock(1),
IrOp::ToR,
IrOp::FromR,
IrOp::RFetch,
IrOp::LoopJ,
IrOp::ForthLocalGet(0),
IrOp::ForthLocalSet(0),
IrOp::ForthFLocalGet(0),
IrOp::ForthFLocalSet(0),
IrOp::Emit,
IrOp::Dot,
IrOp::Cr,
IrOp::Type,
IrOp::Execute,
IrOp::SpFetch,
IrOp::RpFetch,
IrOp::FDup,
IrOp::FDrop,
IrOp::FSwap,
IrOp::FOver,
IrOp::FAdd,
IrOp::FSub,
IrOp::FMul,
IrOp::FDiv,
IrOp::FNegate,
IrOp::FAbs,
IrOp::FSqrt,
IrOp::FMin,
IrOp::FMax,
IrOp::FFloor,
IrOp::FRound,
IrOp::FZeroEq,
IrOp::FZeroLt,
IrOp::FEq,
IrOp::FLt,
IrOp::FetchFloat,
IrOp::StoreFloat,
IrOp::StoF,
IrOp::FtoS,
];
let out = format_ir(&ops);
assert_eq!(out.lines().count(), ops.len());
// Every line non-empty, no accidental blank rendering.
assert!(out.lines().all(|l| !l.trim().is_empty()));
}
}
File diff suppressed because it is too large Load Diff
+55 -202
View File
@@ -1,10 +1,8 @@
#![allow(dead_code)] #![allow(dead_code)]
//! Cross-engine comparison tests: WAFER vs gforth (and `SwiftForth` for perf). //! Cross-engine comparison tests: WAFER vs gforth.
//! //!
//! Validates that WAFER produces identical output to gforth for standard //! Validates that WAFER produces identical output to gforth for standard
//! Forth programs, and benchmarks performance of the engines. `SwiftForth` //! Forth programs, and benchmarks performance of both engines.
//! (`sf64`, native-code commercial compiler) joins the performance report
//! as an upper-bound reference when installed.
//! //!
//! WAFER-only correctness: `cargo test -p wafer-core --test comparison` //! WAFER-only correctness: `cargo test -p wafer-core --test comparison`
//! Full comparison + perf: `cargo test -p wafer-core --test comparison -- --nocapture --ignored` //! Full comparison + perf: `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
@@ -65,48 +63,6 @@ fn find_gforth_fast() -> Option<&'static str> {
.as_deref() .as_deref()
} }
// -----------------------------------------------------------------------
// SwiftForth (sf64) discovery (cached)
// -----------------------------------------------------------------------
static SF64_PATH: OnceLock<Option<String>> = OnceLock::new();
/// Probe sf64 by piping `bye` via stdin — sf64 has no `-e` flag; it takes
/// Forth source from stdin or as bare command-line arguments.
fn probe_sf64(candidate: &str) -> bool {
run_via_stdin(candidate, "bye\n").is_some_and(|o| o.status.success())
}
fn find_sf64() -> Option<&'static str> {
SF64_PATH
.get_or_init(|| {
for candidate in &["/Applications/ForthInc/SwiftForth/bin/macos/sf64", "sf64"] {
if probe_sf64(candidate) {
return Some(candidate.to_string());
}
}
None
})
.as_deref()
}
/// Spawn `binary`, write `input` to its stdin, and collect the output.
fn run_via_stdin(binary: &str, input: &str) -> Option<std::process::Output> {
Command::new(binary)
// Perf lanes measure unguarded code (only the wafer binary reads this)
.env("WAFER_STACK_GUARDS", "0")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(input.as_bytes())?;
child.wait_with_output()
})
.ok()
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Engine runners // Engine runners
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -453,26 +409,6 @@ fn programs() -> Vec<Program> {
expected: "99 \n", expected: "99 \n",
category: Category::Definitions, category: Category::Definitions,
}, },
Program {
name: "search-order-hides",
code: "WORDLIST CONSTANT MY-WL\n\
MY-WL SET-CURRENT\n\
: SECRET 42 ;\n\
FORTH-WORDLIST SET-CURRENT\n\
[UNDEFINED] SECRET . CR\n\
GET-ORDER MY-WL SWAP 1+ SET-ORDER\n\
[DEFINED] SECRET . CR\n\
SECRET . CR\n\
-1 SET-ORDER\n\
[UNDEFINED] SECRET . CR",
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 -- // -- Strings --
Program { Program {
name: "s-quote-type", name: "s-quote-type",
@@ -644,81 +580,6 @@ fn compare_all_programs() {
); );
} }
// -----------------------------------------------------------------------
// Cross-engine behavioral comparison (requires SwiftForth sf64) -- WS-003
// -----------------------------------------------------------------------
/// Run Forth code through `SwiftForth`. Piped sf64 is quiet (no banner, no
/// `ok` echo), truncates input lines at ~256 chars, and exits 243 after an
/// error, so statements are fed one per line with a final `bye`.
fn run_sf64_code(sf64: &str, code: &str) -> Option<EngineResult> {
let mut input = String::new();
for line in code.lines() {
let t = line.trim();
if !t.is_empty() {
input.push_str(t);
input.push('\n');
}
}
input.push_str("bye\n");
let out = run_via_stdin(sf64, &input)?;
Some(EngineResult {
output: String::from_utf8_lossy(&out.stdout).to_string(),
success: out.status.success(),
})
}
/// Correctness lane against `SwiftForth`: the same program corpus as the
/// gforth comparison, sf64 as the oracle. Skips gracefully when sf64 is
/// not installed (CI/linux). Programs listed in `SF64_SKIP` use words or
/// output conventions `SwiftForth` does not share.
#[test]
#[ignore = "requires SwiftForth sf64 (run with -- --ignored)"]
fn compare_all_programs_sf64() {
// dot-quote: `."` outside a definition is a no-op in SwiftForth
// (compile-only); WAFER supports the interpret-mode extension.
const SF64_SKIP: &[&str] = &["dot-quote"];
let Some(sf64) = find_sf64() else {
eprintln!("SKIP: sf64 not found");
return;
};
let progs = programs();
let mut passed = 0;
let mut skipped = 0;
for prog in &progs {
if SF64_SKIP.contains(&prog.name) {
skipped += 1;
continue;
}
let wafer = run_wafer(prog.code);
assert!(wafer.success, "{}: WAFER execution failed", prog.name);
let Some(sf) = run_sf64_code(sf64, prog.code) else {
skipped += 1;
continue;
};
if !sf.success {
eprintln!(" WARN {}: sf64 execution failed, skipping", prog.name);
skipped += 1;
continue;
}
// SwiftForth prints numbers space-prefixed and echoes piped input
// lines, so byte-exact comparison is meaningless; compare the
// whitespace-token stream (the printed values and strings).
let wafer_tokens: Vec<&str> = wafer.output.split_whitespace().collect();
let sf_tokens: Vec<&str> = sf.output.split_whitespace().collect();
assert_eq!(
wafer_tokens, sf_tokens,
"{}: output differs\n WAFER: {:?}\n sf64: {:?}",
prog.name, wafer.output, sf.output
);
passed += 1;
}
eprintln!(
"\nsf64 behavioral comparison: {passed} passed, {skipped} skipped (of {})",
progs.len()
);
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Performance comparison (requires gforth) // Performance comparison (requires gforth)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -837,11 +698,31 @@ fn measure_wafer_release(wafer: &str, bench: &PerfBenchmark) -> Option<u64> {
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
); );
let output = run_via_stdin(wafer, &code)?; let output = Command::new(wafer)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(code.as_bytes())?;
child.wait_with_output()
})
.ok()?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) let stdout = String::from_utf8_lossy(&output.stdout);
let mut times: Vec<u64> = stdout
.trim()
.lines()
.filter_map(|l| l.trim().parse::<u64>().ok())
.collect();
times.sort();
if times.is_empty() {
return None;
}
Some(times[times.len() / 2])
} }
/// Measure WAFER execution time after CONSOLIDATE (direct calls between all words). /// Measure WAFER execution time after CONSOLIDATE (direct calls between all words).
@@ -853,17 +734,21 @@ fn measure_wafer_consolidated(wafer: &str, bench: &PerfBenchmark) -> Option<u64>
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
); );
let output = run_via_stdin(wafer, &code)?; let output = Command::new(wafer)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(code.as_bytes())?;
child.wait_with_output()
})
.ok()?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) let stdout = String::from_utf8_lossy(&output.stdout);
}
/// Parse the microsecond values printed by TIMED-BENCH (one per line) and
/// return the median.
fn median_printed_time(stdout: &[u8]) -> Option<u64> {
let stdout = String::from_utf8_lossy(stdout);
let mut times: Vec<u64> = stdout let mut times: Vec<u64> = stdout
.trim() .trim()
.lines() .lines()
@@ -893,28 +778,18 @@ fn measure_gforth(gforth: &str, bench: &PerfBenchmark) -> Option<u64> {
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) let stdout = String::from_utf8_lossy(&output.stdout);
} // Parse the 3 timing values and take the median
let mut times: Vec<u64> = stdout
/// Measure `SwiftForth` (`sf64`) execution time using Forth-level `ucounter` .trim()
/// (double-cell microsecond counter; `2swap d- drop` yields elapsed us — .lines()
/// the same wrapper shape as gforth's `utime`). Timing excludes startup. .filter_map(|l| l.trim().parse::<u64>().ok())
/// sf64 has no `-e` flag, so the program is piped via stdin — one statement .collect();
/// per line, because sf64 truncates input lines at ~256 chars. times.sort();
/// Returns microseconds, or None if sf64 is unavailable or fails. if times.is_empty() {
fn measure_sf64(sf64: &str, bench: &PerfBenchmark) -> Option<u64> {
let code = format!(
"{define}\n{run}\n\
: TIMED-BENCH ucounter {run} ucounter 2swap d- drop . cr ;\n\
TIMED-BENCH\nTIMED-BENCH\nTIMED-BENCH\nbye\n",
define = bench.define,
run = bench.run_code,
);
let output = run_via_stdin(sf64, &code)?;
if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) Some(times[times.len() / 2])
} }
#[test] #[test]
@@ -955,31 +830,18 @@ fn performance_report() {
); );
} }
let sf64 = find_sf64(); let sep = "=".repeat(80);
if sf64.is_none() { let thin = "-".repeat(80);
eprintln!("NOTE: sf64 (SwiftForth) not found — column skipped");
}
let sep = "=".repeat(100);
let thin = "-".repeat(100);
println!("\n{sep}"); println!("\n{sep}");
println!(" WAFER vs Gforth vs SwiftForth Performance Comparison (release mode)"); println!(" WAFER vs Gforth Performance Comparison (release mode)");
println!("{sep}\n"); println!("{sep}\n");
println!( println!(
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}", "{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
"Benchmark", "Benchmark", "WAFER", "CONSOL", "gforth", "gf-fast", "WAFER/gf", "limit"
"WAFER",
"CONSOL",
"gforth",
"gf-fast",
"sf64",
"WAFER/gf",
"WAFER/sf",
"limit"
); );
println!( println!(
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}", "{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
"", "(us)", "(us)", "(us)", "(us)", "(us)", "", "", "" "", "(us)", "(us)", "(us)", "(us)", "", ""
); );
println!("{thin}"); println!("{thin}");
@@ -994,11 +856,9 @@ fn performance_report() {
.unwrap_or(0); .unwrap_or(0);
let gf = gforth.and_then(|g| measure_gforth(g, bench)); let gf = gforth.and_then(|g| measure_gforth(g, bench));
let gf_fast = gforth_fast.and_then(|g| measure_gforth(g, bench)); let gf_fast = gforth_fast.and_then(|g| measure_gforth(g, bench));
let sf = sf64.and_then(|s| measure_sf64(s, bench));
let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}")); let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}")); let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}"));
let sf_str = sf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
let best_wafer = if consol > 0 && consol < wafer { let best_wafer = if consol > 0 && consol < wafer {
consol consol
} else { } else {
@@ -1012,15 +872,11 @@ fn performance_report() {
} }
}); });
let ratio = ratio_val.map_or_else(|| "-".to_string(), |r| format!("{r:.2}x")); let ratio = ratio_val.map_or_else(|| "-".to_string(), |r| format!("{r:.2}x"));
let sf_ratio = sf.filter(|&s| s > 0).map_or_else(
|| "-".to_string(),
|s| format!("{:.2}x", best_wafer as f64 / s as f64),
);
let limit_str = format!("{:.2}x", bench.max_ratio); let limit_str = format!("{:.2}x", bench.max_ratio);
println!( println!(
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}", "{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
bench.name, wafer, consol, gf_str, gf_fast_str, sf_str, ratio, sf_ratio, limit_str bench.name, wafer, consol, gf_str, gf_fast_str, ratio, limit_str
); );
// Check regression limits // Check regression limits
@@ -1043,9 +899,6 @@ fn performance_report() {
println!("{thin}"); println!("{thin}");
println!(" WAFER = all optimizations, CONSOL = after CONSOLIDATE"); println!(" WAFER = all optimizations, CONSOL = after CONSOLIDATE");
println!(" WAFER/gf = best(WAFER,CONSOL) vs gforth, < 1.0 means WAFER faster"); println!(" WAFER/gf = best(WAFER,CONSOL) vs gforth, < 1.0 means WAFER faster");
println!(
" WAFER/sf = best(WAFER,CONSOL) vs SwiftForth sf64 (native code; informational, no limit)"
);
println!("{sep}\n"); println!("{sep}\n");
if !regressions.is_empty() { if !regressions.is_empty() {
+1 -6
View File
@@ -105,13 +105,8 @@ fn expected_load_failures(path: &str) -> u32 {
// TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as // TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as
// unknown-word errors. Fix the SOURCE/`>IN` interaction with // unknown-word errors. Fix the SOURCE/`>IN` interaction with
// line-mode input and drop this to 0. // line-mode input and drop this to 0.
//
// The 38th: line 368 `R> DROP TRUE` runs interpreted (its enclosing
// definition aborted on the missing NAME?), and the bare `R>` used
// to underflow the return stack silently; stack guards now report
// it as "Return stack underflow (throw -6)".
if path.ends_with("/toolstest.fth") { if path.ends_with("/toolstest.fth") {
return 38; return 37;
} }
0 0
} }
+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.5", default-features = false, features = ["crypto"] } wafer-core = { path = "../core", version = "0.1.0", 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 }
+4 -8
View File
@@ -6,9 +6,8 @@ use send_wrapper::SendWrapper;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wafer_core::config::WaferConfig; use wafer_core::config::WaferConfig;
use wafer_core::memory::{CELL_SIZE, PAD_BASE, PAD_SIZE, SYSVAR_BASE_VAR}; use wafer_core::memory::{CELL_SIZE, PAD_BASE, PAD_SIZE};
use wafer_core::outer::ForthVM; use wafer_core::outer::ForthVM;
use wafer_core::runtime::Runtime;
use wafer_core::runtime::{HostAccess, HostFn}; use wafer_core::runtime::{HostAccess, HostFn};
use crate::runtime_web::WebRuntime; use crate::runtime_web::WebRuntime;
@@ -54,12 +53,9 @@ impl WaferRepl {
/// Get the current number base (10 = decimal, 16 = hex). /// Get the current number base (10 = decimal, 16 = hex).
pub fn base(&mut self) -> u32 { pub fn base(&mut self) -> u32 {
self.vm.runtime_mut().mem_read_i32(SYSVAR_BASE_VAR) as u32 // BASE is stored at SYSVAR_BASE_VAR in WASM memory
} self.vm.take_output(); // no-op side effect; just return base
10 // TODO: read from memory once we have a getter
/// Names of all user-facing words (visible, non-internal), newest first.
pub fn words(&self) -> Vec<String> {
self.vm.word_names()
} }
/// Reset the VM to initial state. /// Reset the VM to initial state.
+2 -19
View File
@@ -38,23 +38,6 @@ 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());
@@ -151,7 +134,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| call_error(fn_index, &e))?; .map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?;
Ok(()) Ok(())
} }
} }
@@ -423,7 +406,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| call_error(fn_index, &e))?; .map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?;
Ok(()) Ok(())
} }
+23 -42
View File
@@ -1,11 +1,8 @@
import init, { WaferRepl } from './pkg/wafer_web.js'; import init, { WaferRepl } from './pkg/wafer_web.js';
let repl = null; let repl = null;
const HISTORY_KEY = 'wafer-history'; const history = [];
const HISTORY_MAX = 200; let historyIdx = -1;
const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
let historyIdx = history.length;
let builtinWords = null;
const WORD_CATEGORIES = { const WORD_CATEGORIES = {
'Stack': 'DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER PICK ROLL DEPTH .S'.split(' '), 'Stack': 'DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER PICK ROLL DEPTH .S'.split(' '),
@@ -42,12 +39,10 @@ function updateStack() {
if (!repl) return; if (!repl) return;
try { try {
const stack = repl.data_stack(); const stack = repl.data_stack();
const base = repl.base();
const suffix = base !== 10 ? ` [base ${base}]` : '';
if (stack.length === 0) { if (stack.length === 0) {
stackBar.textContent = `Stack: (empty)${suffix}`; stackBar.textContent = 'Stack: (empty)';
} else { } else {
stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}${suffix}`; stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}`;
} }
} catch { } catch {
stackBar.textContent = 'Stack: (error)'; stackBar.textContent = 'Stack: (error)';
@@ -55,25 +50,19 @@ function updateStack() {
} }
function updateUserWords() { function updateUserWords() {
const list = document.getElementById('user-word-list'); const cat = document.getElementById('cat-user');
if (!list || !repl || !builtinWords) return; if (!cat) return;
list.innerHTML = ''; // We'll track user words by checking what the REPL evaluates
for (const w of repl.words()) { // For now, just show the category
if (!builtinWords.has(w)) list.appendChild(wordChip(w));
}
} }
function evaluate(line, record = true) { function evaluate(line) {
if (!repl) return; if (!repl) return;
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed) return; if (!trimmed) return;
// Add to history (user-typed lines only; skip consecutive duplicates) // Add to history
if (record && history[history.length - 1] !== trimmed) { history.push(trimmed);
history.push(trimmed);
if (history.length > HISTORY_MAX) history.splice(0, history.length - HISTORY_MAX);
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
}
historyIdx = history.length; historyIdx = history.length;
try { try {
@@ -94,7 +83,6 @@ function evaluate(line, record = true) {
updatePrompt(); updatePrompt();
updateStack(); updateStack();
updateUserWords();
} }
// Input handling // Input handling
@@ -128,18 +116,6 @@ document.getElementById('btn-toggle-words').addEventListener('click', () => {
document.getElementById('word-panel').classList.toggle('collapsed'); document.getElementById('word-panel').classList.toggle('collapsed');
}); });
function wordChip(w) {
const chip = document.createElement('span');
chip.className = 'word-chip';
chip.textContent = w;
chip.title = w;
chip.addEventListener('click', () => {
input.value += (input.value.length > 0 ? ' ' : '') + w;
input.focus();
});
return chip;
}
function buildWordPanel() { function buildWordPanel() {
const container = document.getElementById('word-categories'); const container = document.getElementById('word-categories');
container.innerHTML = ''; container.innerHTML = '';
@@ -153,7 +129,15 @@ function buildWordPanel() {
const list = document.createElement('div'); const list = document.createElement('div');
list.className = 'word-list'; list.className = 'word-list';
for (const w of words) { for (const w of words) {
list.appendChild(wordChip(w)); const chip = document.createElement('span');
chip.className = 'word-chip';
chip.textContent = w;
chip.title = w;
chip.addEventListener('click', () => {
input.value += (input.value.length > 0 ? ' ' : '') + w;
input.focus();
});
list.appendChild(chip);
} }
cat.appendChild(list); cat.appendChild(list);
container.appendChild(cat); container.appendChild(cat);
@@ -195,7 +179,7 @@ document.getElementById('btn-run-init').addEventListener('click', () => {
if (code.trim()) { if (code.trim()) {
// Run each line separately // Run each line separately
for (const line of code.split('\n')) { for (const line of code.split('\n')) {
if (line.trim()) evaluate(line, false); if (line.trim()) evaluate(line);
} }
} }
localStorage.setItem('wafer-init-code', code); localStorage.setItem('wafer-init-code', code);
@@ -230,7 +214,6 @@ document.getElementById('btn-reset').addEventListener('click', () => {
appendLine('WAFER reset.', 'line-ok'); appendLine('WAFER reset.', 'line-ok');
updatePrompt(); updatePrompt();
updateStack(); updateStack();
updateUserWords();
} catch (e) { } catch (e) {
appendLine(`Reset error: ${e.message}`, 'line-error'); appendLine(`Reset error: ${e.message}`, 'line-error');
} }
@@ -242,8 +225,6 @@ async function boot() {
try { try {
await init(); await init();
repl = new WaferRepl(); repl = new WaferRepl();
// Everything defined at boot is "builtin"; later definitions are user words
builtinWords = new Set(repl.words());
output.innerHTML = ''; output.innerHTML = '';
appendLine('WAFER — WebAssembly Forth Engine in Rust', 'line-output'); appendLine('WAFER — WebAssembly Forth Engine in Rust', 'line-output');
appendLine(`Type Forth at the > prompt. Press ? for help.`, 'line-output'); appendLine(`Type Forth at the > prompt. Press ? for help.`, 'line-output');
@@ -260,7 +241,7 @@ async function boot() {
const initCode = document.getElementById('init-code').value; const initCode = document.getElementById('init-code').value;
if (initCode.trim()) { if (initCode.trim()) {
for (const line of initCode.split('\n')) { for (const line of initCode.split('\n')) {
if (line.trim()) evaluate(line, false); if (line.trim()) evaluate(line);
} }
localStorage.setItem('wafer-init-code', initCode); localStorage.setItem('wafer-init-code', initCode);
} }
@@ -271,7 +252,7 @@ async function boot() {
const code = atob(location.hash.slice(1)); const code = atob(location.hash.slice(1));
document.getElementById('init-code').value = code; document.getElementById('init-code').value = code;
for (const line of code.split('\n')) { for (const line of code.split('\n')) {
if (line.trim()) evaluate(line, false); if (line.trim()) evaluate(line);
} }
} catch { /* ignore bad hash */ } } catch { /* ignore bad hash */ }
} }
+2 -2
View File
@@ -18,11 +18,11 @@ confidence-threshold = 0.8
[bans] [bans]
multiple-versions = "deny" multiple-versions = "deny"
wildcards = "deny" wildcards = "deny"
# Transitive duplicates from wasmtime v47 dependencies # Transitive duplicates from wasmtime v31 -- will resolve when upgrading
skip = [ skip = [
"getrandom", "getrandom",
"syn",
"hashbrown", "hashbrown",
"r-efi",
"thiserror", "thiserror",
"thiserror-impl", "thiserror-impl",
"wasm-encoder", "wasm-encoder",
+10 -32
View File
@@ -33,10 +33,7 @@ contexts:
- include: compare - include: compare
- include: memory - include: memory
- include: io - include: io
- include: pictured
- include: string_ops
- include: float - include: float
- include: tools
- include: dictionary - include: dictionary
- include: exception - include: exception
- include: parsing - include: parsing
@@ -98,31 +95,27 @@ contexts:
# Quotations (Core-Ext 6.2.0455): [: ... ;] compiles an anonymous word. # Quotations (Core-Ext 6.2.0455): [: ... ;] compiles an anonymous word.
- match: '(?i)(?:^|(?<=\s))(\[:|;\]){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(\[:|;\]){{ident_break}}'
scope: keyword.other.definition.forth scope: keyword.other.definition.forth
- match: '(?i)(?:^|(?<=\s))(VARIABLE|2VARIABLE|CONSTANT|2CONSTANT|VALUE|CREATE|DEFER|MARKER|REMEMBER|BUFFER:|FCONSTANT|FVARIABLE)(\s+)(\S+)?' - match: '(?i)(?:^|(?<=\s))(VARIABLE|2VARIABLE|CONSTANT|2CONSTANT|VALUE|CREATE|DEFER|MARKER|BUFFER:|FCONSTANT|FVARIABLE)(\s+)(\S+)?'
captures: captures:
1: keyword.other.defining.forth 1: keyword.other.defining.forth
3: entity.name.constant.forth 3: entity.name.constant.forth
- match: '(?i)(?:^|(?<=\s))(DOES>|IMMEDIATE|RECURSE|POSTPONE|COMPILE,|LITERAL|2LITERAL|FLITERAL|SLITERAL|DEFER!|DEFER@){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(DOES>|IMMEDIATE|RECURSE|POSTPONE|COMPILE,|LITERAL|2LITERAL|FLITERAL|SLITERAL){{ident_break}}'
scope: keyword.other.defining.forth scope: keyword.other.defining.forth
control: control:
- match: '(?i)(?:^|(?<=\s))(IF|THEN|ELSE|BEGIN|UNTIL|WHILE|REPEAT|AGAIN|DO|\?DO|LOOP|\+LOOP|LEAVE|UNLOOP|EXIT|CASE|OF|ENDOF|ENDCASE|QUIT){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(IF|THEN|ELSE|BEGIN|UNTIL|WHILE|REPEAT|AGAIN|DO|\?DO|LOOP|\+LOOP|LEAVE|UNLOOP|EXIT|CASE|OF|ENDOF|ENDCASE|QUIT){{ident_break}}'
scope: keyword.control.forth scope: keyword.control.forth
# Conditional compilation (Tools-ext 15.6.2).
- match: '(?i)(?:^|(?<=\s))(\[IF\]|\[ELSE\]|\[THEN\]|\[DEFINED\]|\[UNDEFINED\]){{ident_break}}'
scope: keyword.control.conditional-compilation.forth
stack_ops: stack_ops:
- match: '(?i)(?:^|(?<=\s))(DUP|\?DUP|DROP|SWAP|OVER|ROT|-ROT|NIP|TUCK|PICK|ROLL|2DUP|2DROP|2SWAP|2OVER|2ROT|DEPTH|SP@){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(DUP|\?DUP|DROP|SWAP|OVER|ROT|-ROT|NIP|TUCK|PICK|ROLL|2DUP|2DROP|2SWAP|2OVER|2ROT|DEPTH|SP@){{ident_break}}'
scope: support.function.stack.forth scope: support.function.stack.forth
return_stack: return_stack:
# RP@ / RDEPTH are WAFER extensions (gforth-style return-stack access). - match: '(?i)(?:^|(?<=\s))(>R|R>|R@|2>R|2R>|2R@|N>R|NR>|I|J|CS-PICK|CS-ROLL){{ident_break}}'
- match: '(?i)(?:^|(?<=\s))(>R|R>|R@|2>R|2R>|2R@|N>R|NR>|I|J|CS-PICK|CS-ROLL|RP@|RDEPTH){{ident_break}}'
scope: support.function.return-stack.forth scope: support.function.return-stack.forth
arithmetic: arithmetic:
- match: '(?i)(?:^|(?<=\s))(\+|-|\*|/|MOD|/MOD|\*/|\*/MOD|NEGATE|ABS|MIN|MAX|1\+|1-|2\*|2/|M\*|M\+|M\*/|UM\*|UM/MOD|FM/MOD|SM/REM|S>D|D>S|D\+|D-|DNEGATE|DABS|DMAX|DMIN|D2\*|D2/){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(\+|-|\*|/|MOD|/MOD|\*/|\*/MOD|NEGATE|ABS|MIN|MAX|1\+|1-|2\*|2/|M\*|M\+|M\*/|UM\*|UM/MOD|FM/MOD|SM/REM|S>D|D>S){{ident_break}}'
scope: keyword.operator.arithmetic.forth scope: keyword.operator.arithmetic.forth
logic: logic:
@@ -130,36 +123,21 @@ contexts:
scope: keyword.operator.logical.forth scope: keyword.operator.logical.forth
compare: compare:
- match: '(?i)(?:^|(?<=\s))(=|<>|<|>|<=|>=|U<|U>|0=|0<>|0<|0>|D<|D=|D0<|D0=|DU<|WITHIN){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(=|<>|<|>|<=|>=|U<|U>|0=|0<>|0<|0>){{ident_break}}'
scope: keyword.operator.comparison.forth scope: keyword.operator.comparison.forth
memory: memory:
- match: '(?i)(?:^|(?<=\s))(@|!|C@|C!|\+!|2@|2!|C,|ALLOT|HERE|ALIGN|ALIGNED|CELL\+|CELLS|CHAR\+|CHARS|UNUSED|MOVE|CMOVE|CMOVE>|FILL|ERASE|BLANK|ALLOCATE|FREE|RESIZE|PAD){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(@|!|C@|C!|\+!|2@|2!|ALLOT|HERE|ALIGN|ALIGNED|CELL\+|CELLS|CHAR\+|CHARS|UNUSED|MOVE|CMOVE|CMOVE>|FILL|ERASE|BLANK|ALLOCATE|FREE|RESIZE|PAD){{ident_break}}'
scope: support.function.memory.forth scope: support.function.memory.forth
io: io:
- match: '(?i)(?:^|(?<=\s))(EMIT|CR|SPACE|SPACES|TYPE|\.|U\.|\.R|U\.R|D\.|D\.R|\?|KEY|KEY\?|PAGE|AT-XY|ACCEPT|EXPECT|\.S|F\.S|\.RS){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(EMIT|CR|SPACE|SPACES|TYPE|\.|U\.|\.R|U\.R|D\.|D\.R|\?|KEY|KEY\?|PAGE|AT-XY|ACCEPT|EXPECT|\.S){{ident_break}}'
scope: support.function.io.forth scope: support.function.io.forth
# Pictured numeric output (6.1: <# # #S #> HOLD SIGN; HOLDS is Core-Ext).
pictured:
- match: '(?i)(?:^|(?<=\s))(<#|#>|#S|#|HOLD|HOLDS|SIGN){{ident_break}}'
scope: support.function.pictured.forth
# String word set (17.6).
string_ops:
- match: '(?i)(?:^|(?<=\s))(COUNT|COMPARE|-TRAILING|/STRING){{ident_break}}'
scope: support.function.string.forth
float: float:
- match: '(?i)(?:^|(?<=\s))(F\+|F-|F\*\*|F\*|F/|FNEGATE|FABS|FMAX|FMIN|FSQRT|FFLOOR|FROUND|FLOOR|FSINCOS|FSINH|FSIN|FCOSH|FCOS|FTANH|FTAN|FASINH|FASIN|FACOSH|FACOS|FATANH|FATAN2|FATAN|FEXPM1|FEXP|FLNP1|FLN|FLOG|FALOG|F=|F<|F0=|F0<|F~|FDUP|FDROP|FSWAP|FOVER|FROT|FNIP|FTUCK|FDEPTH|F@|F!|FE\.|FS\.|F\.|F>D|D>F|F>S|S>F|>FLOAT|REPRESENT|PRECISION|SET-PRECISION|FALIGN|FALIGNED|DFALIGN|DFALIGNED|SFALIGN|SFALIGNED|FLOAT\+|FLOATS|DFLOAT\+|DFLOATS|SFLOAT\+|SFLOATS|DF@|DF!|SF@|SF!){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(F\+|F-|F\*|F/|FNEGATE|FABS|FMAX|FMIN|FSQRT|FFLOOR|FROUND|FSINCOS|F=|F<|F0=|F0<|F~|FDUP|FDROP|FSWAP|FOVER|FROT|FNIP|FTUCK|FDEPTH|F@|F!|FE\.|FS\.|F\.|F>D|D>F|F>S|S>F|>FLOAT|REPRESENT|PRECISION|SET-PRECISION|FALIGNED|DFALIGNED|SFALIGNED|DF@|DF!|SF@|SF!){{ident_break}}'
scope: support.function.float.forth scope: support.function.float.forth
# Interactive/debug tools (Tools word set + WAFER REPL additions).
tools:
- match: '(?i)(?:^|(?<=\s))(SEE-IR|SEE|DUMP|BYE|HELP){{ident_break}}'
scope: support.function.tools.forth
dictionary: dictionary:
- match: "(?i)(?:^|(?<=\\s))('|\\[']|,|>BODY|FIND|WORDS|ONLY|ALSO|PREVIOUS|DEFINITIONS|FORTH|GET-ORDER|SET-ORDER|GET-CURRENT|SET-CURRENT|WORDLIST|SEARCH-WORDLIST|FORTH-WORDLIST|ENVIRONMENT\\?|EXECUTE){{ident_break}}" - match: "(?i)(?:^|(?<=\\s))('|\\[']|,|>BODY|FIND|WORDS|ONLY|ALSO|PREVIOUS|DEFINITIONS|FORTH|GET-ORDER|SET-ORDER|GET-CURRENT|SET-CURRENT|WORDLIST|SEARCH-WORDLIST|FORTH-WORDLIST|ENVIRONMENT\\?|EXECUTE){{ident_break}}"
scope: support.function.dictionary.forth scope: support.function.dictionary.forth
@@ -169,7 +147,7 @@ contexts:
scope: keyword.control.exception.forth scope: keyword.control.exception.forth
parsing: parsing:
- match: '(?i)(?:^|(?<=\s))(PARSE|PARSE-NAME|WORD|REFILL|EVALUATE|INCLUDE|INCLUDED|SOURCE|SOURCE-ID|>IN|BASE|DECIMAL|HEX|STATE|>NUMBER|SEARCH|SUBSTITUTE|UNESCAPE|REPLACES|S){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(PARSE|PARSE-NAME|WORD|REFILL|EVALUATE|SOURCE|SOURCE-ID|>IN|BASE|STATE|>NUMBER|SEARCH|SUBSTITUTE|UNESCAPE|REPLACES|S){{ident_break}}'
scope: support.function.parsing.forth scope: support.function.parsing.forth
literals: literals:
@@ -207,5 +185,5 @@ contexts:
wafer_extras: wafer_extras:
# WAFER-specific extensions beyond the Forth 2012 standard. # WAFER-specific extensions beyond the Forth 2012 standard.
# When the language grows new user-facing non-standard words, add them here. # When the language grows new user-facing non-standard words, add them here.
- match: '(?i)(?:^|(?<=\s))(CONSOLIDATE|RANDOM|RND-SEED|UTIME|READ-PASSWORD|EMPTY|GILD){{ident_break}}' - match: '(?i)(?:^|(?<=\s))(CONSOLIDATE|RANDOM|RND-SEED|UTIME|READ-PASSWORD){{ident_break}}'
scope: support.function.wafer-extra.forth scope: support.function.wafer-extra.forth