release: 0.2.8
CI / check (push) Has been cancelled

Ships the self-guard expansion, and corrects what the benchmark tables claim.
Measured with wafer, gforth and SwiftForth all native on x86-64 -- the macOS
sf64 build runs under Rosetta 2 and flatters us -- Fibonacci is 1.16x rather
than 0.83x, so sf64 still wins it and wafer takes the other four. README and
OPTIMIZATIONS now carry both tables.
This commit is contained in:
Oleksandr Kozachuk
2026-08-10 12:48:33 +02:00
parent e963e636d3
commit 4f96f8860a
8 changed files with 68 additions and 24 deletions
+7 -3
View File
@@ -5,7 +5,7 @@ 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).
## [Unreleased]
## [0.2.8] - 2026-08-10
### Added
@@ -17,8 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
which computes the same thing: the callee would have run the guard, taken
the branch and returned. In fib's tree the leaves are half of all nodes.
Fibonacci(25) 356 -> 237 µs, which takes the last benchmark that was behind
SwiftForth `sf64` past it: 1.24x -> 0.83x, five of five.
Fibonacci(25) 356 -> 237 µs on the arm64 development machine, where that
reads 1.24x -> 0.83x of SwiftForth `sf64`. Measured again with **both
engines native on x86-64** -- the macOS `sf64` build runs under Rosetta 2,
which flatters WAFER -- Fibonacci is 1.16x, so it remains the one benchmark
of the five that `sf64` wins. See the two tables in the README.
The guard runs twice along the recursive path, so it has to be small (at
most six operations) and free of effects -- no calls, no memory, no
@@ -360,6 +363,7 @@ 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.8]: https://github.com/ok2/wafer/compare/v0.2.7...v0.2.8
[0.2.7]: https://github.com/ok2/wafer/compare/v0.2.6...v0.2.7
[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
+1 -1
View File
@@ -2,7 +2,7 @@
## What is WAFER?
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, self-guard expansion for recursive words, consolidation). Beats gforth on all benchmarks in release mode (3-20x), and SwiftForth `sf64` on all five. Includes a browser-based REPL via wasm-pack.
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, self-guard expansion for recursive words, consolidation). Beats gforth on every benchmark, and SwiftForth `sf64` on four of five (measured native-vs-native on x86-64; the macOS sf64 build is x86-64 under Rosetta and flatters WAFER). Includes a browser-based REPL via wasm-pack.
## Architecture
Generated
+3 -3
View File
@@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wafer"
version = "0.2.7"
version = "0.2.8"
dependencies = [
"anyhow",
"clap",
@@ -1600,7 +1600,7 @@ dependencies = [
[[package]]
name = "wafer-core"
version = "0.2.7"
version = "0.2.8"
dependencies = [
"anyhow",
"insta",
@@ -1615,7 +1615,7 @@ dependencies = [
[[package]]
name = "wafer-web"
version = "0.2.7"
version = "0.2.8"
dependencies = [
"anyhow",
"js-sys",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.2.7"
version = "0.2.8"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ok2/wafer"
+32 -8
View File
@@ -8,7 +8,7 @@ An optimizing Forth 2012 compiler targeting WebAssembly. WAFER JIT-compiles each
- **200+ words** across 12 Forth 2012 word sets, all at **100% compliance**
- **Optimizing compiler** with 6 IR passes + stack-to-local promotion (per region, so a hot loop keeps its registers even inside a word that does I/O; `DO` and `BEGIN` loops alike) + consolidation
- **Faster than gforth** on all benchmarks in release mode (3-20x), and past SwiftForth `sf64` on all five
- **Faster than gforth** on every benchmark, and past SwiftForth `sf64` -- a native-code compiler -- on four of five
- **JIT compilation** — each `:` definition compiles to its own WASM module
- **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect`
- **Typed calling convention** — a word with a statically known stack effect passes its stack items as WASM values, so a call keeps them in registers instead of round-tripping through memory
@@ -80,8 +80,29 @@ git submodule update --init
## Performance
WAFER beats gforth (the GNU Forth reference implementation) on all benchmarks in release mode, and
SwiftForth `sf64`, which compiles to native code, on all five:
WAFER beats gforth (the GNU Forth reference implementation) on every benchmark, and SwiftForth
`sf64` -- which compiles to native code -- on four of the five.
Measured with all three engines running **native x86-64**, on an idle 16-vCPU Xeon Platinum 8124M
@ 3.0 GHz (median of three runs):
```
Benchmark WAFER gforth sf64 WAFER/gf WAFER/sf
Fibonacci(25) 411 3221 355 0.13x 1.16x
Factorial(12)x100K 994 7141 3058 0.14x 0.33x
GCD-bench(20K) 1591 3211 2423 0.50x 0.66x
NestedLoops(50)x1K 889 6824 2342 0.13x 0.38x
Collatz(2K) 391 3981 1659 0.10x 0.24x
```
Times in microseconds; WAFER is the better of the JIT and `CONSOLIDATE` runs. Below 1.0 means WAFER
is faster. Fibonacci is the one WAFER loses: it is one call per node with no loop to promote, and
`sf64` keeps its stack in registers across a call the way only a native code generator can.
Fibonacci, GCD and Collatz held to within 2% across the three runs; Factorial and NestedLoops are
softer, since `sf64` varied by half there, but they are wide wins either way.
`just bench-compare` on the development machine (M1 Ultra, arm64) reports different numbers, and
they flatter WAFER:
```
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
@@ -92,11 +113,14 @@ NestedLoops(50)x1K 501 509 7092 1898 0.07x
Collatz(2K) 196 190 3955 633 0.05x 0.30x
```
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CONSOLIDATE`.
The only SwiftForth build for macOS is x86-64 under Rosetta 2, while WAFER and gforth are native
arm64 -- so that `sf64` column is native against emulated. The gap is not small, and it lands
exactly where it matters: Fibonacci reads 0.83x there and 1.16x when neither engine is emulated.
Treat the arm64 table as what the regression limits in `comparison.rs` are calibrated against, and
the x86-64 table as what to believe about the engines.
Two caveats on the `sf64` column. The SwiftForth build here is x86-64 running under Rosetta 2
while WAFER and gforth are native arm64, so it is a native-vs-emulated comparison; and sf64
uses 64-bit cells to WAFER's 32-bit.
A caveat applies to both: `sf64` uses 64-bit cells to WAFER's 32-bit, so WAFER does less work per
operation.
A word whose stack effect is statically known gets a **typed entry point**: its stack items travel in and out
as WASM values instead of through the memory data stack, so cranelift keeps them in registers across a call
@@ -108,7 +132,7 @@ Call-heavy code is what this pays for -- Fibonacci went from 4.3x slower than `s
Recursive words then get one more thing: their base-case guard is tested at the **call site**, so a
leaf of the recursion costs a comparison instead of a call. `: FIB DUP 2 < IF EXIT THEN ... RECURSE`
compiles its `RECURSE` as `DUP 2 < IF ELSE RECURSE THEN`, which is what the callee would have done
on entry anyway. Half of fib's nodes are leaves, and that is the last 1.4x.
on entry anyway. Half of fib's nodes are leaves, and that is worth 1.4x.
## Testing
+1 -1
View File
@@ -9,7 +9,7 @@ license.workspace = true
workspace = true
[dependencies]
wafer-core = { path = "../core", version = "0.2.7" }
wafer-core = { path = "../core", version = "0.2.8" }
wasmtime = { workspace = true }
anyhow = { workspace = true }
clap = { version = "4", features = ["derive"] }
+1 -1
View File
@@ -12,7 +12,7 @@ workspace = true
crate-type = ["cdylib", "rlib"]
[dependencies]
wafer-core = { path = "../core", version = "0.2.7", default-features = false, features = ["crypto"] }
wafer-core = { path = "../core", version = "0.2.8", default-features = false, features = ["crypto"] }
wasm-bindgen = "0.2"
js-sys = "0.3"
send_wrapper = { workspace = true }
+22 -6
View File
@@ -511,12 +511,27 @@ The guard runs twice along the recursive path: once at the call site, once insid
### Impact
Fibonacci(25): 356 to 237 microseconds. In fib's tree half of all nodes are leaves, which is where the factor comes from. This is what took the last benchmark past `sf64`.
Fibonacci(25): 356 to 237 microseconds on the arm64 development machine. In fib's tree half of all nodes are leaves, which is where the factor comes from. It does not take Fibonacci past `sf64`, though the arm64 table below says otherwise: with both engines native on x86-64, Fibonacci reads 1.16x and stays the one benchmark `sf64` wins.
## Current Performance vs Gforth
All optimizations enabled, release mode, measured with UTIME:
All three engines **native x86-64**, idle 16-vCPU Xeon Platinum 8124M @ 3.0 GHz,
median of three runs:
```
Benchmark WAFER gforth sf64 WAFER/gf WAFER/sf
Fibonacci(25) 411 3221 355 0.13x 1.16x
Factorial(12)x100K 994 7141 3058 0.14x 0.33x
GCD-bench(20K) 1591 3211 2423 0.50x 0.66x
NestedLoops(50)x1K 889 6824 2342 0.13x 0.38x
Collatz(2K) 391 3981 1659 0.10x 0.24x
```
The same suite on the arm64 development machine (M1 Ultra), which is what the
regression limits in `comparison.rs` are calibrated against:
```
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
Fibonacci(25) 237 242 3340 287 0.07x 0.83x
@@ -526,11 +541,12 @@ NestedLoops(50)x1K 501 509 7092 1898 0.07x
Collatz(2K) 196 190 3955 633 0.05x 0.30x
```
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. `sf64` is SwiftForth,
which compiles to native code; two caveats on that column. The install here is an
x86-64 binary under Rosetta 2 while WAFER and gforth are native arm64, so it is a
native-vs-emulated comparison and a native SwiftForth would be faster than these
numbers; and sf64 uses 64-bit cells to WAFER's 32-bit.
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. The two tables
disagree because the only SwiftForth build for macOS is x86-64 under Rosetta 2
while WAFER and gforth are native arm64 -- and the emulation penalty lands
hardest on the call-heavy benchmark, so Fibonacci reads 0.83x on arm64 and 1.16x
when neither engine is emulated. Believe the x86-64 table about the engines. One
caveat holds for both: sf64 uses 64-bit cells to WAFER's 32-bit.
## Remaining Opportunities