waforth/README.md

181 lines
6.2 KiB
Markdown
Raw Normal View History

2022-04-17 20:44:43 +02:00
<img src="./doc/logo.svg" height="64">
2022-05-01 08:32:36 +02:00
# [WAForth](https://mko.re/waforth): Forth Interpreter+Compiler for WebAssembly
2018-05-23 19:47:23 +02:00
2022-05-05 20:27:51 +02:00
[![Build](https://github.com/remko/waforth/actions/workflows/build.yml/badge.svg)](https://github.com/remko/waforth/actions/workflows/build.yml)
2022-04-17 20:44:43 +02:00
WAForth is a small bootstrapping Forth interpreter and dynamic compiler for
2018-05-23 19:47:23 +02:00
[WebAssembly](https://webassembly.org). You can see it in a demo
2022-05-01 08:32:36 +02:00
[here](https://mko.re/waforth/).
2018-05-23 19:47:23 +02:00
2020-01-04 19:57:07 +01:00
It is [entirely written in (raw)
WebAssembly](https://github.com/remko/waforth/blob/master/src/waforth.wat), and
the compiler generates WebAssembly code on the fly. The only parts for which it
relies on external (JavaScript) code is to dynamically load modules (since
WebAssembly [doesn't support JIT
yet](https://webassembly.org/docs/future-features/#platform-independent-just-in-time-jit-compilation)),
2019-11-08 22:47:01 +01:00
and the I/O primitives to read and write a character to a screen.
2018-05-23 19:47:23 +02:00
2022-04-17 20:44:43 +02:00
The WebAssembly module containing the interpreter, dynamic compiler, and
2022-05-01 08:32:36 +02:00
all built-in words comes down to 13k (6k gzipped), with an extra 7k (3k gzipped) for the JavaScript wrapper and web UI.
2022-04-17 20:44:43 +02:00
2019-03-15 13:09:02 +01:00
WAForth is still in an experimental stage. It implements most of the [ANS Core
Words](http://lars.nocrew.org/dpans/dpans6.htm#6.1), and passes most of the
2022-04-17 20:44:43 +02:00
[Forth 200x Test Suite](https://forth-standard.org/standard/testsuite)
2019-03-15 13:09:02 +01:00
core word tests.
2022-05-05 22:23:27 +02:00
![WAForth Console](doc/console.gif "WAForth Console")
## Using WAForth in an application
You can embed WAForth in any JavaScript application.
A simple example to illustrate starting WAForth, and binding JavaScript functions:
```typescript
import WAForth from "waforth";
(async () => {
// Create the UI
document.body.innerHTML = `<button>Go!</button><pre></pre>`;
const btn = document.querySelector("button");
const log = document.querySelector("pre");
2018-05-23 19:47:23 +02:00
// Initialize WAForth
const forth = new WAForth();
forth.onEmit = (c) =>
log.appendChild(document.createTextNode(String.fromCharCode(c)));
await forth.load();
// Bind "prompt" call to a function that pops up a JavaScript prompt, and pushes the entered number back on the stack
forth.bind("prompt", (stack) => {
const message = stack.popString();
const result = window.prompt(message);
stack.push(parseInt(result));
});
// Load Forth code to bind the "prompt" call to a word, and call the word
forth.interpret(`
( Call "prompt" with the given string )
: PROMPT ( c-addr u -- n )
S" prompt" SCALL
;
( Prompt the user for a number, and write it to output )
: ASK-NUMBER ( -- )
S" Please enter a number" PROMPT
." The number was" SPACE .
;
`);
btn.addEventListener("click", () => {
forth.interpret("ASK-NUMBER");
});
})();
```
## Development
### Install Dependencies
2018-05-23 19:47:23 +02:00
2019-11-08 22:47:01 +01:00
The build uses the [WebAssembly Binary
Toolkit](https://github.com/WebAssembly/wabt) for converting raw WebAssembly
text format into the binary format, and [Yarn](https://yarnpkg.com) for
managing the dependencies of the shell.
2018-05-23 19:47:23 +02:00
2019-11-08 22:47:01 +01:00
brew install wabt yarn
2018-05-23 19:47:23 +02:00
yarn
### Building & Running
2018-05-23 19:47:23 +02:00
To build everything:
make
To run the development server:
2022-04-13 17:02:46 +02:00
make dev
2018-05-23 19:47:23 +02:00
### Testing
2018-05-23 19:47:23 +02:00
2022-04-30 13:18:48 +02:00
The tests are served from `/waforth/tests` by the development server.
2019-03-10 10:37:01 +01:00
You can also run the tests in Node.JS by running
make check
2018-05-23 19:47:23 +02:00
## Design
2019-03-09 22:02:46 +01:00
The WAForth core is written as [a single
module](https://github.com/remko/waforth/blob/master/src/waforth.wat) in
WebAssembly's [text
2019-11-11 11:48:29 +01:00
format](https://webassembly.github.io/spec/core/text/index.html).
2018-05-23 19:47:23 +02:00
### The Interpreter
The interpreter runs a loop that processes commands, and switches to and from
compiler mode.
2019-03-09 22:02:46 +01:00
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.
2018-05-23 19:47:23 +02:00
### The Compiler
2019-03-09 22:02:46 +01:00
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](https://webassembly.org/docs/future-features/#platform-independent-just-in-time-jit-compilation),
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](https://webassembly.github.io/spec/core/valid/modules.html#tables) 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.
2019-11-08 22:47:01 +01:00
This of course introduces some overhead, although it appears limited.
2019-03-09 22:02:46 +01:00
As WebAssembly doesn't support unstructured jumps, control flow words
(`IF/ELSE/THEN`, `LOOP`, `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.
2018-05-23 19:47:23 +02:00
2018-05-31 21:11:04 +02:00
Finally, the compiler adds minimal debug information about the compiled word in
2019-03-09 22:02:46 +01:00
the [name
section](https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#name-section),
making it easier for doing some debugging in the browser.
2018-05-31 21:11:04 +02:00
2019-03-09 22:02:46 +01:00
![Debugger view of a compiled
2022-04-17 18:22:36 +02:00
word](doc/debugger.png "Debugger view of a
2019-03-09 22:02:46 +01:00
compiled word")
2018-05-31 21:11:04 +02:00
2018-05-23 19:47:23 +02:00
### The Loader
2019-03-09 22:02:46 +01:00
The loader is a small bit of JavaScript that uses the [WebAssembly JavaScript
API](https://webassembly.github.io/spec/js-api/index.html) to dynamically load
a compiled word (in the form of a WebAssembly module), and ensuring that the
shared function table is large enough for the module to register itself.
2018-05-23 19:47:23 +02:00
### The Shell
2019-03-09 22:02:46 +01:00
The shell is [a JavaScript
class](https://github.com/remko/waforth/blob/master/src/shell/WAForth.js) that
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 `interpret()` function to execute a fragment of
2019-03-09 22:02:46 +01:00
Forth code.
2018-05-23 19:47:23 +02:00
2019-03-09 22:02:46 +01:00
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
2022-05-01 08:32:36 +02:00
in action [here](https://mko.re/waforth/).
2018-05-23 19:47:23 +02:00