This commit is contained in:
Remko Tronçon 2018-05-23 19:47:23 +02:00
parent f4eed729ea
commit bfb70d6c9c
8 changed files with 153 additions and 63 deletions

View file

@ -19,7 +19,7 @@ tests: $(WASM_FILES)
.PHONY: sieve-vanilla
sieve-vanilla: $(WASM_FILES)
$(PARCEL) --no-hmr -o dist/sieve-vanilla.html benchmarks/sieve-vanilla/index.html
$(PARCEL) --no-hmr -o dist/sieve-vanilla.html tests/benchmarks/sieve-vanilla/index.html
wasm: $(WASM_FILES) src/tools/quadruple.wasm.hex
@ -27,7 +27,7 @@ dist/waforth.wasm: src/waforth.wat dist
racket -f $< > src/waforth.wat.tmp
$(WAT2WASM) $(WAT2WASM_FLAGS) -o $@ src/waforth.wat.tmp
dist/sieve-vanilla.wasm: benchmarks/sieve-vanilla/sieve-vanilla.wat
dist/sieve-vanilla.wasm: tests/benchmarks/sieve-vanilla/sieve-vanilla.wat
$(WAT2WASM) $(WAT2WASM_FLAGS) -o $@ $<
dist:

105
README.md
View file

@ -1 +1,104 @@
Nothing to see here (yet)
# [WAForth](https://el-tramo.be/waforth): Forth Interpreter+Compiler for WebAssembly
WAForth is a bootstrapping Forth interpreter and compiler for
[WebAssembly](https://webassembly.org). You can see it in a demo
[here](https://el-tramo.be/waforth/).
It is (almost) entirely written in WebAssembly and Forth, and the compiler generates
WebAssembly code on the fly. The only parts for which it relies on external
(JavaScript) code is the dynamic loader (since WebAssembly [doesn't support JIT
yet](https://webassembly.org/docs/future-features/#platform-independent-just-in-time-jit-compilation)), and the I/O primitives to read and write a character.
The implementation was influenced by [jonesforth](http://git.annexia.org/?p=jonesforth.git;a=summary),
and I shamelessly stole the Forth implementation of some of its high-level words.
WAForth is still just an experiment, and doesn't implement all the ANS standard words yet.
## Install Dependencies
The build uses [Racket](https://racket-lang.org) for processing the WebAssembly code,
the [WebAssembly Binary Toolkit](https://github.com/WebAssembly/wabt) for converting it in binary
format,and [Yarn](https://yarnpkg.com) for managing the dependencies of the shell.
brew install wabt yarn minimal-racket
yarn
## Building & Running
To build everything:
make
To run the development server:
make dev-server
## Testing
To run the development server with all the tests:
make tests
## Design
### The Macro Assembler
The WAForth core is written as [a single module](https://github.com/remko/waforth/blob/master/src/waforth.wat) in WebAssembly's [text format](https://webassembly.github.io/spec/core/text/index.html). The
text format isn't really meant for writing code in, so it has no facilities like a real assembler
(e.g. constant definitions, macro expansion, ...) However, since the text format uses S-expressions,
you can do some small modifications to make it extensible with Lisp-style macros.
I added some Racket macros to the module definition, and implemented [a mini
assembler](https://github.com/remko/waforth/blob/master/src/tools/assembler.rkt)
to print out the resulting s-expressions in the right format.
The result is something that looks like a standard WebAssembly module, but sprinkled with some
macros for convenience.
### The Interpreter
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 a strict threading system
for executing code. WebAssembly doesn't allow unstructured jumps, let alone dynamic jumps.
Instead, each word is implemented as a single WebAssembly function, and the system uses
calls and indirect calls (see below) to execute words.
### The Compiler
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. This of course introduces
some overhead, although it seems limited.
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.
### The Loader
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 add it to the shared function table.
### The Shell
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 `run()` function to execute a fragment of Forth code.
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](https://el-tramo.be/waforth/).
![WAForth Console](https://el-tramo.be/waforth/console.gif "WAForth Console")

View file

@ -40,6 +40,11 @@ body {
color: #00ff00;
}
.jqconsole-header a {
color: inherit;
text-decoration: none;
}
/* The cursor. */
.jqconsole-cursor {
background-color: gray;

View file

@ -4,6 +4,7 @@ import $ from "jquery";
window.jQuery = $;
require("jq-console");
// Copied from https://rosettacode.org/wiki/Sieve_of_Eratosthenes#Forth
const sieve = `
: prime? HERE + C@ 0= ;
: composite! HERE + 1 SWAP C! ;
@ -30,6 +31,9 @@ const forth = new WAForth();
let jqconsole = $("#console").jqconsole("WAForth\n", "");
$("#console").hide();
$(".jqconsole-header").html(
"<span><a target='_blank' href='https://github.com/remko/waforth'>WAForth</a>\n</span>"
);
let outputBuffer = [];
forth.onEmit = c => {
outputBuffer.push(String.fromCharCode(c));

View file

@ -1159,67 +1159,45 @@ EOF
(unreachable)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; For benchmarking
;; A sieve with direct calls. Only here for benchmarking
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(func $sieve1_prime
(call $here)
(call $plus)
(call $c-fetch)
(call $zero-equals))
(func $sieve1_composite
(call $here)
(call $plus)
(call $push (i32.const 1))
(call $swap)
(call $c-store))
(func $sieve1 (export "sieve1")
(call $here)
(call $over)
(call $erase)
(call $push (i32.const 2))
(block $label$1
(loop $label$2
(call $two-dupe)
(call $dupe)
(call $star)
(call $greater-than)
(br_if $label$1 (i32.eqz (call $pop)))
(call $dupe)
(call $sieve1_prime)
(if (i32.ne (call $pop) (i32.const 0))
(block
(call $two-dupe)
(call $dupe)
(call $star)
(call $beginDo)
(block $label$4
(loop $label$5
(call $i)
(call $sieve1_composite)
(call $dupe)
(br_if $label$4 (call $endDo (call $pop)))
(br $label$5)))))
(call $one-plus)
(br $label$2)))
(call $drop)
(call $push (i32.const 1))
(call $swap)
(call $push (i32.const 2))
(call $beginDo)
(block $label$6
(loop $label$7
(call $i)
(call $sieve1_prime)
(if (i32.ne (call $pop) (i32.const 0))
(block
(call $drop)
(call $i)))
(br_if $label$6 (call $endDo (i32.const 1)))
(br $label$7))))
(!def_word "sieve1" "$sieve1")
; (func $sieve1_prime
; (call $here) (call $plus) (call $c-fetch) (call $zero-equals))
;
; (func $sieve1_composite
; (call $here) (call $plus) (call $push (i32.const 1)) (call $swap) (call $c-store))
;
; (func $sieve1 (export "sieve1")
; (call $here) (call $over) (call $erase)
; (call $push (i32.const 2))
; (block $label$1
; (loop $label$2
; (call $two-dupe) (call $dupe) (call $star) (call $greater-than)
; (br_if $label$1 (i32.eqz (call $pop)))
; (call $dupe) (call $sieve1_prime)
; (if (i32.ne (call $pop) (i32.const 0))
; (block
; (call $two-dupe) (call $dupe) (call $star)
; (call $beginDo)
; (block $label$4
; (loop $label$5
; (call $i) (call $sieve1_composite) (call $dupe)
; (br_if $label$4 (call $endDo (call $pop)))
; (br $label$5)))))
; (call $one-plus)
; (br $label$2)))
; (call $drop)
; (call $push (i32.const 1)) (call $swap) (call $push (i32.const 2))
; (call $beginDo)
; (block $label$6
; (loop $label$7
; (call $i) (call $sieve1_prime)
; (if (i32.ne (call $pop) (i32.const 0))
; (block (call $drop) (call $i)))
; (br_if $label$6 (call $endDo (i32.const 1)))
; (br $label$7))))
; (!def_word "sieve1" "$sieve1")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -1,6 +1,6 @@
(module
(import "js" "print" (func $print (param i32)))
(memory 4096)
(memory 8192)
(func $sieve (export "sieve") (param $n i32) (result i32)
(local $i i32)
(local $j i32)