• Stars
    star
    487
  • Rank 90,029 (Top 2 %)
  • Language WebAssembly
  • License
    MIT License
  • Created over 6 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Small but complete dynamic Forth Interpreter/Compiler for and in WebAssembly

WAForth: Forth Interpreter+Compiler for WebAssembly

Build

WAForth is a small but complete Forth interpreter and dynamic compiler for and in WebAssembly. You can see it in action in an interactive Forth console, in a Logo-like Turtle graphics language, and in an interactive notebook.

WAForth is entirely written in (raw) WebAssembly, and the compiler generates WebAssembly code on the fly. The only parts for which it relies on external code is to dynamically load modules (since WebAssembly doesn't support JIT yet), and the I/O primitives to read and write a character to a screen.

The WebAssembly module containing the interpreter, dynamic compiler, and all built-in words comes down to 14k (7k gzipped), with an extra 15k (7k gzipped) for the JavaScript wrapper, web UI, and encoding overhead.

WAForth implements all ANS Core Words (and passes Forth 200x Test Suite core word tests), and many ANS Core Extension Words. You can get the complete list of supported words from the interactive console.

You can watch a video of a talk at FOSDEM 2023 introducing WAForth, and explaining the goals and some of the internals.

You can read more about the internals and the design of WAForth in the Design document.

Thurtle program
WAForth integrated in Thurtle, a turtle graphics programming environment using Forth

Standalone shell

Although WebAssembly (and therefore WAForth) is typically used in a web environment (web browsers, Node.js), WAForth also has a standalone native command-line shell. You can download a pre-built binary of the standalone shell from the Releases page.

The standalone shell uses the Wasmtime engine, but its build configuration can easily be adapted to build using any WebAssembly engine that supports the WebAssembly C API (although some engines have known issues).

Thurtle program
Standalone WAForth shell executable

Native compiler

Besides just-in-time compilation (in a browser or native), WAForth can also be used to compile Forth ahead-of-time. waforthc is a tool that uses WAForth to compile a Forth program into a native executable. WebAssembly is used as the host runtime platform and intermediate representation during compilation, and then compiled into an executable that no longer contains any WebAssembly infrastructure.

Using WAForth in a JavaScript application

You can embed WAForth in any JavaScript application.

A simple example (CodePen) to illustrate starting WAForth, and binding JavaScript functions:

import WAForth, { withLineBuffer } from "waforth";

(async () => {
  // Create the UI
  document.body.innerHTML = `<button>Go!</button><pre></pre>`;
  const btn = document.querySelector("button");
  const log = document.querySelector("pre");

  // Initialize WAForth
  const forth = new WAForth();
  forth.onEmit = withLineBuffer((c) =>
    log.appendChild(document.createTextNode(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");
  });
})();

Asynchronous bindings

For asynchronous bindings, use bindAsync instead of bind.

bindAsync expects an execution token on the stack, which is to be called with a success flag after the bound function is called. This is illustrated in the fetch example:

forth.bindAsync("ip?", async () => {
  const result = await (
    await fetch("https://api.ipify.org?format=json")
  ).json();
  forth.pushString(result.ip);
});

forth.interpret(`
( IP? callback. Called after IP address was received )
: IP?-CB ( true c-addr n | false -- )
  IF 
    ." Your IP address is " TYPE CR
  ELSE
    ." Unable to fetch IP address" CR
  THEN
;

( Fetch the IP address, and print it to console )
: IP? ( -- )
  ['] IP?-CB
  S" ip?" SCALL 
;
`);

Writing WebAssembly in Forth

WAForth supports directly writing WebAssembly in Forth using the CODE word. For example, the following snippet defines a raw WebAssembly version of DUP:

CODE DUP' ( n -- n n )
  \ Put pointer to top of Forth stack (local_0) on the 
  \ Wasm operand stack (for use later)
  [ 0 ] $LOCAL.GET

  \ Load the number at the top of the Forth stack 
  \ (local_0 - 4) on the Wasm operand stack
  [ 0 ] $LOCAL.GET
  [ 4 ] $I32.CONST
  $I32.SUB
  $I32.LOAD

  \ Store the number on the Wasm operand stack on 
  \ top of the Forth stack. The first operand (Forth 
  \ stack pointer) was put on the Wasm operand stack 
  \ at the beginning of this snippet
  $I32.STORE

  \ Increment the Forth top of stack pointer (local_0), 
  \ and leave it on the Wasm operand stack as return value
  [ 0 ] $LOCAL.GET
  [ 4 ] $I32.CONST 
  $I32.ADD
;CODE

This creates a word with the specified WebAssembly:

(func $DUP' (param $tos i32) (result i32)
  local.get $tos
  local.get $tos
  i32.const 4
  i32.sub
  i32.load
  i32.store
  local.get $tos
  i32.const 4
  i32.add
)

Note that support for writing WebAssembly is still experimental. The assembly words used in the above snippet ($LOCAL.GET, $I32.*, ...) aren't available in the WAForth core, and have to be manually defined using the low-level $U, and $S, words that append (LEB128-encoded) bytes directly to the WebAssembly module. For example, the code above relies on the following assembly word definitions:

: $LOCAL.GET ( u -- )   32 $U, $U,         ; IMMEDIATE
: $I32.ADD   ( -- )    106 $U,             ; IMMEDIATE
: $I32.SUB   ( -- )    107 $U,             ; IMMEDIATE
: $I32.CONST ( n -- )   65 $U, $S,         ; IMMEDIATE
: $I32.LOAD  ( -- )     40 $U, 2 $U, 0 $U, ; IMMEDIATE
: $I32.STORE ( -- )     54 $U, 2 $U, 0 $U, ; IMMEDIATE

The exact opcodes and format of instructions can be found in the WebAssembly spec. In the future, I'll probably make all WebAssembly assembly instructions available somewhere. Using WebAssembly locals also currently isn't possible, although this can be added in the future.

Notebooks

The WAForth Visual Studio Code Extension adds support for interactive Forth notebooks powered by WAForth. This lets you create documents that combine rich text with executable Forth code. You can execute both text-based Forth code, as well as Thurtle graphics.

Because it is powered by WebAssembly, this extension works both in the desktop version of Visual Studio Code and in the browser version of Visual Studio Code (e.g. https://github.dev, https://vscode.dev).

You can also convert the notebook into a lightweight self-contained page using wafnb2html. An example can be seen here.

Goals

Here are some of the goals (and non-goals) of WAForth:

  • WebAssembly-first: Implement as much as possible in (raw) WebAssembly. Only call out to JavaScript (or whatever the host language is) for functionality that is not available in WebAssembly (I/O, loading compiled WebAssembly code).
  • Simplicity: Keep the code as simple and clean as possible. Raw WebAssembly requires more effort to maintain than code in a high level language, so avoid complexity if you can.
  • Completeness: Implement a complete (and correct) Forth system, following the ANS Standard, including all ANS Core words.
  • Speed: If some speed gains can be gotten without paying much in simplicity (e.g. better design of the system, more efficient implementation of words, simple compiler improvements, ...), then I do it. However, generating the most efficient code would require a smart compiler, and a smart compiler would introduce a lot of complexity if implemented in raw WebAssembly, so speed is not an ultimate goal. Although the low level of WebAssembly gives some speed advantages, the design of the system will cause execution to consist almost exclusively of indirect calls to small functions, so there will be languages targeting WebAssembly that run faster.
  • Binary size: Since the entire system is written in raw WebAssembly, and since one of the main goals is simplicity, the resulting binary size is naturally quite small (±12k). However, I don't do any special efforts to save bytes here and there in the code (or the generated code) if it makes things more complex.
  • Ease of use: Like most Forths, I currently don't do much effort to provide functionality to make Forth programming easy and safe (helpful errors, stacktraces, strict bounds checks, ...). However, the compiler emits debug information to help step through the WebAssembly code of words, and I hope to add more debugging aids to the compiler in the future (if it doesn't add too much complexity)

Debugger view of a compiled word

Development

Install Dependencies

The build uses the WebAssembly Binary Toolkit for converting raw WebAssembly text format into the binary format, and Yarn (and therefore Node.JS) for managing the build process and the dependencies of the shell.

brew install wabt yarn
yarn

Building & Running

To build everything:

yarn build

To run the development server:

yarn dev

Testing

The tests are served from /waforth/tests by the development server.

You can also run the tests in Node.JS by running

yarn test

More Repositories

1

age-plugin-se

Age plugin for Apple's Secure Enclave
Swift
113
star
2

wigit

Simple & light Git-based wiki
PHP
113
star
3

kburns

Generate slideshow movie with Ken Burns effect
Ruby
80
star
4

dsadmin

Google Cloud Datastore Emulator Administration UI
TypeScript
61
star
5

fancy-dmg

Tools for making fancy DMG images
Makefile
57
star
6

atomail

Convert (E-Mail) messages into RSS feeds
Python
52
star
7

uxn.wasm

WebAssembly implementation of the Uxn virtual machine
WebAssembly
36
star
8

go-mkvparse

Fast Matroska parser in Go
Go
34
star
9

cloudrun-slog

Example Cloud Run Go app with lightweight structured logging using `slog`
Go
28
star
10

smtp-http-proxy

Tiny SMTP to HTTP bridge
C++
27
star
11

qlmka

macOS Quick Look plugin for Matroska `.mka` files
C
26
star
12

be-non-apple

Belgian (Non-Apple) Keyboard Layout
25
star
13

react-swift-example

Example Universal/Isomorphic React app on a Swift Vapor backend
JavaScript
23
star
14

docbook-kit

Collection of DocBook writing utilities & style(s)
XSLT
22
star
15

xmpp-tdg-examples

Code Examples from "XMPP: The Definitive Guide"
Python
19
star
16

react-go-gae-example

Example Universal/Isomorphic React app on a Google App Engine Standard Go backend
Go
15
star
17

gulp-hashsum

Gulp plugin to generate checksum files
JavaScript
14
star
18

retjilp

Native auto-retweet bot
Ruby
13
star
19

phkp

PHKP: A HKP keyserver in PHP
PHP
12
star
20

scons2ninja

Generate Ninja build files for SCons
Python
11
star
21

node-itunes-db

Fast iTunes XML Parser
C++
9
star
22

blog-skeleton

My Blog Code/Skeleton
CSS
9
star
23

mixing-cocoa-and-qt

Mixing Cocoa and Qt example
C++
9
star
24

qttestutil

Convenience classes for easier unit testing using QtTest
C++
8
star
25

swift-duktape

Low-level Swift wrapper for Duktape
Ruby
6
star
26

json-array-streams

Streams for JSON array parsing/stringifying
JavaScript
5
star
27

pycotap

Tiny Python TAP Test Runner
Python
5
star
28

itunes-scripts

iTunes Scripts
AppleScript
4
star
29

svnfeed

Generate RSS feeds of (remote) Subversion repositories
Python
4
star
30

toys

Toy programs (for experiments etc.)
C++
3
star
31

dsbackups

Package to work with Google Cloud Datastore exports
Go
3
star
32

lgmtray

Lightweight GMail Notifier
C++
3
star
33

fy-shuffle

Customizable Fisher-Yates shuffle
JavaScript
2
star
34

unicode-dash

Unicode Characters Dash DocSet
Python
2
star
35

plainize

Light but powerful HTML to plain text conversion
JavaScript
2
star
36

jsnprsr

JsnPrsr: A Lean JSON Parser
Java
2
star
37

xep227-to-prosody

Script to convert XEP-0227 data to Prosody data format
Python
1
star
38

qr.cgi

A Web QR Code Generator
C++
1
star
39

webob-dash

WebOb Dash Docset
Makefile
1
star
40

lcg-random

(Predictable) LCG Random Number Generator
JavaScript
1
star
41

webapp2-dash

WebApp2 Dash Docset
Makefile
1
star
42

openfire-vcardextpres

Openfire vCard-based Extended Presence plugin
Java
1
star
43

webtest-dash

WebTest Dash Docset
Makefile
1
star
44

nds

Google Cloud Datastore Go utility clients
Go
1
star
45

dicewords

Generate DiceWare word lists
Go
1
star
46

go-sni

Go implementation of Freedesktop.org StatusNotifierItem specification
Go
1
star
47

gemsite

Remko's Gemini Capsule
Go
1
star
48

beautiful-xmpp-testing

Beautiful XMPP Testing
C++
1
star
49

password-generator

Diceware & Memorable Random-letter Passphrase generator
JavaScript
1
star
50

ecppunit

CPPUnit support for Eclipse CDT
Java
1
star