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.
Version bump plus a doc sweep: the benchmark tables in README and
docs/OPTIMIZATIONS.md were still from before the typed calling convention,
OPTIMIZATIONS listed BEGIN loop promotion as not started, and the
subroutine-threading section of docs/WAFER.md described the memory ABI as
the only one.
Two reporting bugs found from the browser shell.
An uncaught ABORT printed 'ABORT (throw -1)'. The standard defines ABORT
as 'empty the data stack and perform the function of QUIT', and QUIT
displays no message; gforth and SwiftForth are both silent. It now takes
the same silent path QUIT got in 0.2.5. CATCH still reports -1 and still
restores the stack depth, and ABORT" still prints its text -- different
word, different code (-2).
Compile-only constructs used in interpretation state claimed to be an
'unknown word', which is misleading for a word the system obviously
knows: ABORT", IF, THEN, LOOP, LITERAL, RECURSE and friends. They now
report 'interpreting a compile-only word: <name> (throw -14)', the
standard condition both reference engines give. The check reuses the
existing INTERPRETER_TOKENS table at the point where interpretation has
already failed, so a genuine typo still reports 'unknown word'.
Ships as v0.2.6.
The CORE word was missing. QUIT empties the return stack, enters
interpretation state, restores SOURCE-ID to the user input device and
returns to the interpreter without a message, leaving the data stack
untouched -- that last part is the whole difference to ABORT, which the
standard defines as 'empty the data stack, then QUIT'.
Implemented on the throw plumbing with the standard code -56, so nested
EVALUATE / INCLUDE frames unwind and are abandoned on the way out. Two
places treat -56 specially: CATCH lets it through (QUIT is a return to
the prompt, not an exception) and evaluate() turns it into a silent Ok
after the compile-state wipe it already performs.
Semantics checked against gforth 0.7.3 and SwiftForth sf64, which agree:
the data stack survives, nothing is printed, the rest of the input is
abandoned, and '1 2 ' QUIT CATCH .' prints nothing while leaving 1 2.
Six tests in outer.rs pin it. Deliberately NOT added to the cross-engine
corpus: what QUIT abandons is the input source, and the three engines are
fed differently there, so a comparison would measure the harness.
The gap survived because the Forth 2012 suite skips QUIT by its own
admission, and HELP's coverage lint compares dictionary against docs --
a word missing from both looks complete. docs/wafer-anki.txt had been
documenting QUIT as if it existed.
ABORT itself was already correct: executed while a definition is open it
clears both stacks and returns to interpretation state.
Ships as v0.2.5.
A host word signals failure by throwing across the JS boundary, and the
browser runtime reported that exception with its Debug form, so an
empty-stack RESIZE surfaced as
call_func(134) failed: JsValue(Error: Stack underflow ...)
with the engine's JS stack trace glued on. The thrown message IS the
Forth message, so take it verbatim: 'Stack underflow', the same text the
native CLI prints. Exceptions without a message keep the call context --
those are genuine runtime faults, not Forth throws.
CATCH is unaffected: it reads the throw code from its own channel rather
than parsing messages. Verified against a fresh VM in Node (initSync +
WaferRepl): host-word underflow, compiled-guard underflow, THROW,
unknown word and ' RESIZE CATCH . all match the native CLI.
Ships as v0.2.4.
`wasm-pack build --release` died with "can't find crate" for rustversion,
then thiserror_impl, then every other proc-macro. Cargo strips debuginfo
from release artifacts by default and on macOS that takes the metadata
proc-macro dylibs need to be loadable with it, so rustc could no longer
open them.
Debug builds are unstripped, which is why the whole test suite stayed
green while the browser REPL could not be built for production at all.
Stripping buys nothing for build scripts and proc-macros, so
[profile.release.build-override] exempts them; release binaries stay
stripped.
Also pins wafer-core to 0.2.3 in wafer-web and wafer-cli — both still
asked for 0.2.1. The caret requirement resolved, so nothing broke.
Core:
- Uncaught THROW prints its standard message ("Stack underflow
(throw -4)"; unknown codes as "Catch = <n>") instead of the
"forth-throw" sentinel. ABORT" text is carried as a structured
payload and shown only when the -2 throw goes uncaught -- CATCH
stays silent and the payload cannot go stale. ABORT throws -1
through the same path.
- WORDS: optional same-line substring filter (WORDS FDEPTH), skips
internal words (new INTERNAL header flag, set at create for
underscore-prefixed names), wraps at 78 columns, prints a count.
ORDER names wids (FORTH / wid#N) instead of Rust debug output.
- .S honors BASE. New: F.S (float stack), DUMP (hex+ASCII,
bounds-checked, 4K cap), ? (fetch-and-print, boot.fth). BYE is a
real word now: sets a VM flag the driver honors (exits REPL,
stops rest of line/file).
CLI:
- Persistent history (~/.local/state/wafer/history, 0600 perms,
$WAFER_HISTORY override), Tab completion over the live dictionary
(snapshot refreshed after each line), Up/Down do prefix history
search, Ctrl-C clears the line instead of exiting.
Web:
- History survives reloads (localStorage, cap 200, dedup, init-code
runs excluded), User Words palette populated via new words()
export, stack bar annotates non-decimal BASE, base() reads the
real BASE sysvar instead of returning a hardcoded 10.
Browser consumers (kelvar) need a host-provided password prompt so the
master never appears on the command line. Exposes a single method:
WaferRepl::set_prompter(js_sys::Function) -> Result<(), JsError>
Given a JS function `(prompt: string) => string`, registers it as the
Forth word `READ-PASSWORD` with stack effect
( prompt-addr prompt-u -- pw-addr pw-u )
The returned bytes land in WAFER's PAD region. Enforces PAD_SIZE-1 as
a hard upper bound — a silent truncation would cause a derived password
to mismatch the one used during setup, which is exactly the failure
mode we are trying to avoid.
`js_sys::Function` is !Send/!Sync but `HostFn` requires both. In a
browser WASM build there is only ever one thread, so wrap it in
`send_wrapper::SendWrapper`, which panics if accessed off-thread — an
honest guard rather than a lie.
Introduces a `crypto` feature (on by default) that wires the RustCrypto
sha1/sha2 crates into a small `HashAlgo` registry. `register_primitives`
iterates `crypto::ALGOS` and installs one Forth host word per algorithm,
each with the stack effect
( c-addr u -- c-addr2 u2 )
reading `u` bytes from `c-addr` and writing the digest into a shared
`HASH_SCRATCH` region in linear memory (carved out between the float
stack and the dictionary).
Adding a new hash is a one-line entry in `ALGOS`. `register_host_primitive`
is now `pub` so downstream crates can extend the VM with their own I/O
host words without forking WAFER — kelvar (a deterministic password
manager on WAFER) is the first consumer.
- 4 unit tests (lib-level sha1/256/512 + registry sanity)
- 5 integration tests (in-VM `SHA1`/`SHA256`/`SHA512` against RFC-3174,
FIPS-180, and the first-round S/KEY seed used by `hel`)
- All 437 existing lib tests still pass; `wafer-web` still builds for
`wasm32-unknown-unknown` with the feature enabled
Implement PAGE (Facility word set) as IR primitive emitting form feed.
Web REPL clears output div on form feed, CLI REPL sends ANSI clear.
Fix init code panel: use default textarea content instead of placeholder
so init code actually executes on first visit. Update wasm-pack 0.10→0.14
and refresh Cargo.lock to latest compatible versions.
Decouple ForthVM from wasmtime via a Runtime trait so the same outer
interpreter, compiler, and 200+ word definitions work on both native
(wasmtime) and browser (js-sys WebAssembly API) backends.
Runtime trait (runtime.rs):
- HostAccess trait for memory/global ops inside host function closures
- HostFn type: Box<dyn Fn(&mut dyn HostAccess) -> Result<()>>
- Runtime trait: memory, globals, table, instantiate, call, register
NativeRuntime (runtime_native.rs):
- Wraps wasmtime Engine/Store/Memory/Table/Global/Func
- CallerHostAccess bridges HostAccess to wasmtime Caller API
- Feature-gated behind "native" (default)
outer.rs refactor:
- ForthVM<R: Runtime> — generic over execution backend
- All 87 host functions converted from Func::new closures to HostFn
- All memory access via rt.mem_read/write_*, global access via rt.get/set_*
- Zero logic changes — pure API conversion
wafer-core feature gates:
- default = ["native"] includes wasmtime + all native modules
- Without "native": pure Rust only (outer, codegen, optimizer, dictionary)
Browser REPL (crates/web):
- WebRuntime: js-sys WebAssembly.Memory/Table/Global/Module/Instance
- WaferRepl: wasm-bindgen entry point (evaluate, data_stack, reset)
- WebAssembly.Function with Safari fallback (wrapper module)
- Frontend: dark terminal UI, word panel, init code editor, history
- Build: wasm-pack build --target web
All 452 tests pass (431 unit + 1 benchmark + 9 comparison + 11 compliance).
Add `wafer build` to compile Forth source files to standalone .wasm modules,
and `wafer run` to execute them. The same .wasm file works with both the
wafer runtime (via wasmtime) and in browsers (via generated JS loader).
New CLI subcommands:
- `wafer build file.fth -o file.wasm` — compile to standalone WASM
- `wafer build file.fth -o file.wasm --js` — also generate JS/HTML loader
- `wafer build file.fth --entry WORD` — custom entry point
- `wafer run file.wasm` — execute pre-compiled module
Entry point resolution: --entry flag > MAIN word > recorded top-level execution.
Memory snapshot embedded as WASM data section preserves VARIABLE/CONSTANT state.
Metadata in custom "wafer" section enables the runner to provide host functions.
New modules: export.rs (orchestration), runner.rs (wasmtime host), js_loader.rs
(browser support). Refactored codegen.rs to share logic between consolidation
and export via compile_multi_word_module(). Added ir_bodies tracking for
VARIABLE, CONSTANT, CREATE, VALUE, DEFER, BUFFER:, MARKER, 2CONSTANT,
2VARIABLE, 2VALUE, FVARIABLE defining words.
Removed dead code: dot_func field, unused wafer-web stub crate, wasmtime-wasi
dependency from CLI, orphaned --consolidate/--output CLI flags.
425 tests pass (414 original + 11 new including 7 round-trip integration tests).
- Add docs/OPTIMIZATIONS.md: catalog of 14 optimization passes with
status tracking and implementation roadmap
- Configure workspace-level clippy and rustc lints in Cargo.toml
- Add clippy.toml and deny.toml for clippy thresholds and dependency
auditing (licenses, advisories, bans)
- Set up pre-commit hook: cargo fmt, dprint, clippy, cargo deny,
cargo machete
- Update Justfile with deny/machete targets, dprint in fmt checks
Optimizing Forth 2012 compiler targeting WebAssembly with IR-based
compilation pipeline, multi-typed stack inference, subroutine threading,
and JIT/consolidation modes. Rust kernel with ~35 primitives and Forth
standard library for core/core-ext word sets.