WAForth: Forth Interpreter+Compiler for WebAssembly
Parts of the implementation were influenced by jonesforth.
brew install wabt yarn yarn
Building & Running
To build everything:
To run the development server:
The tests are served from
/tests by the development server.
You can also run the tests in Node.JS by running
The interpreter runs a loop that processes commands, and switches to and from compiler mode.
Contrary to some other Forth systems, this system doesn't use direct threading for executing code. WebAssembly doesn't allow unstructured jumps, let alone dynamic jumps. Instead, WAForth uses subroutine threading, where each word is implemented as a single WebAssembly function, and the system uses calls and indirect calls (see below) to execute words.
While in compile mode for a word, the compiler generates WebAssembly instructions in binary format (since there is no assembler infrastructure in the browser). Since WebAssembly doesn't support JIT compilation yet, a finished word is bundled into a separate binary WebAssembly module, and sent to the loader, which dynamically loads it and registers it with a shared function table at the next offset, which in turn is recorded in the word dictionary.
Because words reside in different modules, all calls to and from the words need
to happen as indirect
call_indirect calls through the shared function table.
This of course introduces some overhead, although it appears limited.
As WebAssembly doesn't support unstructured jumps, control flow words
REPEAT, ...) can't be implemented in terms of more
basic words, unlike in jonesforth. However, since Forth only requires
structured jumps, the compiler can easily be implemented using the loop and
branch instructions available in WebAssembly.
Finally, the compiler adds minimal debug information about the compiled word in the name section, making it easier for doing some debugging in the browser.
wraps the WebAssembly module, and loads it in the browser. It provides the I/O
primitives to the WebAssembly module to read and write characters to a
terminal, and externally provides a
run() function to execute a fragment of
To tie everything together into an interactive system, there's a small console-based interface around this shell to type Forth code, which you can see in action here.
- The exposed return stack isn't used. Control flow is kept implicitly in the code (e.g. through branches, indirect calls, ...). This also means that control flow can't be influenced by code.