qrforth is a tiny implementation of the Forth programming language that fits in a QR code. The heart of qrforth is a miniaturized Rust library compiled to WebAssembly resulting in a ~1.3kB binary. The qrforth webassembly module is responsible for the Forth stack, primitive words, and functions that provide access to the stack and registers. Javascript is used to execute the webassembly, accept user input, and define new words. The size of all of this comes to ~2.8kB, just small enough to fit in a version 40 QR code (177x177) with the lowest level error correction. Visiting the Home page or scanning the QR code will run qrforth in your browser.
How it Works
The qrforth project consists of three main parts: the wasm module(lib.rs), the html/javascript code(qrforth.html), and the qrforth.com webpage(index.html). The wasm module is encoded in the qrforth.html page and the whole thing is encoded in the QR code. The url in the QR code needs to point to a resource on the web so the qrforth.com webpage is a simple static site hosted on GitHub Pages that serves as a landing page and decodes the url contained in the QR Code.
The url encoded in the QR code looks something like this http://qrforth.com/#i9EKAICqqqrq/26XE5dDdph...
(truncated for readability, full URL here). Using URI fragments the entire qrforth.html webpage is encoded in the URL itself. The qrforth.com webpage receives the URI fragment, decodes and decompresses it, and serves the resulting html as an iframe. All of this is done in the browser with no server side code. The following sections go into more detail on how each part of qrforth works.
To compile our rust library to webassembly we use the --target=wasm32-unknown-unknown --release
cargo build flags. We need to keep the resulting wasm binary as small as possible so we need to: 1) use the #![no_std]
attribute to disable the standard library, 2) define a custom panic handler with #[panic_handler]
, 3) statically allocate our variables to avoid brining in dlmalloc, and 4) strip and optimize our binary with the strip and lto setting in our Cargo.toml file. All of this will keep the rust compiler from adding in typically useful but sizeable code to our wasm binary. I've linked some super helpful resources below that go into more detail on how to do this.
- Rust to WebAssembly the hard way, Surma
- Avoiding allocations in rust to shrink wasm modules, nickb
- A very small rust binary indeed, Graham King
Now what we have our qrforth.wasm binary, we can encode it as a base64 string and put it in our qrforth.html page. The javascript in qrforth.html will decode the base64 string and initialize the wasm module when the browser loads the page. The qrforth.html page is now complete and the URI fragment can be created by compressing the html with brotli and encoding the result as a base64 string. The URL fragment is then appended to the qrforth.com url and the resulting url is encoded as a QR code.
All of these steps are automated with a build script that makes rebuilding as easy as running ./build.sh
. qrforth can also be run locally with a simple python sever and running ./build.sh --local
. More info can be found in the GitHub repository.
Forth Implementation
The qrforth implementation of Forth is designed to be very minimal and only includes a limited subset of the language. Using the words provided, more useful words can be written in qrforth. Some examples are provided herehere. Don't expect qrforth to be a perfect implementation of Forth or ANS Forth compliant, there are already plenty of well designed implementations out there. The goal of qrforth is mainly just to learn more about Forth, WebAssembly, and Rust.
The memory for the system is arbitrarily laid out as an array of 32-bit signed integers, 8000 elements in length. The first 4000 elements reserved for the Forth stack and the second 4000 is general system memory. There is a 8-bit FLAGS
register that is used communicating state from the wasm module to the qrforth html page. User input and output and the Forth dictionary are handled by the page's javascript. The following is a list of the words that make up qrforth:
Word | Definition | Stack Effect |
---|---|---|
push |
Push x onto the stack | ( x -- ) |
+ |
Sum the two numbers at the top of the stack | ( x y -- z ) |
nand |
NAND the two numbers at the top of the stack | ( x y -- z ) |
drop |
Drop the number at the top of the stack | ( x -- ) |
0= |
If top of stack is 0, set to -1, otherwise set to 0 | ( x -- flag ) |
@ |
Fetch memory contents at addr | ( addr -- x ) |
! |
Store x at addr | ( x addr -- ) |
sp@ |
Return stack top pointer to top of stack | ( -- sp ) |
emit |
Print x as an ASCII character | ( x -- ) |
. |
Print top of the stack | ( x -- ) |
key |
Read a character and push it onto the stack | ( -- x ) |
begin |
Marks the start of a begin until loop | ( -- ) |
until |
If x is zero, jumps to matching begin, if x is non-zero, continues execution | ( x -- ) |
if |
If x is -1, continue execution within if then block. | ( x -- ) |
then |
Marks the end of an if then block | ( -- ) |
:
) and semicolon (;
) words are available and are used to define new words in the dictionary. Commenting code is supported using parentheses. You can easily define a new word like dup
to duplicate the top value on the stack:: dup ( x -- x x ) sp@ @ ;