diff --git a/Makefile b/Makefile index 9f46246..a64c3e5 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/README.md b/README.md index 0e79373..750558d 100644 --- a/README.md +++ b/README.md @@ -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") diff --git a/src/shell/index.css b/src/shell/index.css index 9811ab3..1a573c6 100644 --- a/src/shell/index.css +++ b/src/shell/index.css @@ -40,6 +40,11 @@ body { color: #00ff00; } +.jqconsole-header a { + color: inherit; + text-decoration: none; +} + /* The cursor. */ .jqconsole-cursor { background-color: gray; diff --git a/src/shell/index.js b/src/shell/index.js index 50c6d43..b4669a5 100644 --- a/src/shell/index.js +++ b/src/shell/index.js @@ -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( + "WAForth\n" +); let outputBuffer = []; forth.onEmit = c => { outputBuffer.push(String.fromCharCode(c)); diff --git a/src/waforth.wat b/src/waforth.wat index 06485db..b5cc330 100644 --- a/src/waforth.wat +++ b/src/waforth.wat @@ -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") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/benchmarks/sieve-vanilla/index.html b/tests/benchmarks/sieve-vanilla/index.html similarity index 100% rename from benchmarks/sieve-vanilla/index.html rename to tests/benchmarks/sieve-vanilla/index.html diff --git a/benchmarks/sieve-vanilla/index.js b/tests/benchmarks/sieve-vanilla/index.js similarity index 100% rename from benchmarks/sieve-vanilla/index.js rename to tests/benchmarks/sieve-vanilla/index.js diff --git a/benchmarks/sieve-vanilla/sieve-vanilla.wat b/tests/benchmarks/sieve-vanilla/sieve-vanilla.wat similarity index 99% rename from benchmarks/sieve-vanilla/sieve-vanilla.wat rename to tests/benchmarks/sieve-vanilla/sieve-vanilla.wat index 42178ea..2268680 100644 --- a/benchmarks/sieve-vanilla/sieve-vanilla.wat +++ b/tests/benchmarks/sieve-vanilla/sieve-vanilla.wat @@ -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)