mirror of
https://github.com/remko/waforth
synced 2024-11-17 07:48:06 +01:00
Cleanup
This commit is contained in:
parent
f4eed729ea
commit
bfb70d6c9c
8 changed files with 153 additions and 63 deletions
4
Makefile
4
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:
|
||||
|
|
105
README.md
105
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")
|
||||
|
|
|
@ -40,6 +40,11 @@ body {
|
|||
color: #00ff00;
|
||||
}
|
||||
|
||||
.jqconsole-header a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* The cursor. */
|
||||
.jqconsole-cursor {
|
||||
background-color: gray;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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)
|
Loading…
Reference in a new issue