Version 2026.02.24
QWC is a minimal Forth system that can run stand-alone or be embedded into another program.
QWC is implemented in 3 files: (qwc-vm.c, qwc-vm.h, system.c).
The QWC VM is implemented in under 200 lines of code.
QWC has 64 primitives.
The primitives are quite complete and any Forth system can be built from them.
In a QWC program, each instruction is a single CELL.
- By default, a CELL is a QWord, 64-bits, but it can also be 32-bits.
- If <= the last primitive (system), then it is a primitive.
- Else, if it matches a quiet NaN pattern (64-bit only), then it is a literal.
- Else, it is the XT (code address) of a word in the dictionary.
| Word | Action |
|---|---|
| : | Add the next word to the dictionary, set STATE to COMPILE (1). |
| ; | Compile EXIT and change STATE to INTERPRET (0). |
NOTE: '(' skips words until the next ')' word.
NOTE: '\' skips words until the end of the line.
NOTE: Setting STATE to 999 signals QWC to exit.
An INLINE word is somewhat similar to a macro in other languages.
When a word is INLINE, its definition is copied to the target, up to the first EXIT.
When not INLINE, a call is made to the word instead. NOTE: if the next
instruction is EXIT, it becomes a JUMP instead (the tail-call optimization).
Words 't0' through 't9' are transient and are not added to the dictionary.
They are case sensitive: 't0' is a transient word, 'T0' is not.
They help with factoring code and and keep the dictionary uncluttered.
They can be reused as many times as desired.
There are 3 built-in variables x, y, and z. There are also +L and -L that can
be used to create 3 local variables under the user's control. +L and -L can be used
at any time for any reason to create a new frame for new versions of the variables.
On startup, QWC does the following:
- Create 'argc' with the count of command-line arguments
- For each argument, create 'argX' with the address of the argument string
- E.G. "arg0 ztype" will print
qwc - If arg1 exists and names a file that can be opened, load that file.
- Else, try to load file 'qwc-boot.fth' in the local folder '.'.
- Else, try to load file '
BIN_DIRqwc-boot.fth' in the "bin" folder. - On Linux,
BIN_DIRis "/home/chris/bin/". - On Windows,
BIN_DIRis "D:\bin\". BIN_DIRis defined in qwc-vm.h. Adjust it inqwc-vm.hfor your system if needed.
| Primitive | Op/Word | Stack | Description |
|---|---|---|---|
| 0 | exit | (--) | PC = R-TOS. Discard R-TOS. If (PC=0) then stop. |
| 1 | lit | (--) | Push code[PC]. Increment PC. |
| 2 | jmp | (--) | PC = code[PC]. |
| 3 | jmpz | (n--) | If (n==0) then PC = code[PC] else PC = PC+1. |
| 4 | jmpnz | (n--) | If (n!=0) then PC = code[PC] else PC = PC+1. |
| 5 | njmpz | (n--n) | If (n==0) then PC = code[PC] else PC = PC+1. |
| 6 | njmpnz | (n--n) | If (n!=0) then PC = code[PC] else PC = PC+1. |
| 7 | dup | (n--n n) | Duplicate n. |
| 8 | drop | (n--) | Discard n. |
| 9 | swap | (a b--b a) | Swap a and b. |
| 10 | over | (a b--a b a) | Push a. |
| 11 | ! | (n a--) | CELL store n through a. |
| 12 | @ | (a--n) | CELL fetch n through a. |
| 13 | c! | (b a--) | BYTE store b through a. |
| 14 | c@ | (a--b) | BYTE fetch b through a. |
| 15 | >r | (n--) | Move n to the return stack. |
| 16 | r@ | (--n) | Copy n from the return stack. |
| 17 | r> | (--n) | Move n from the return stack. |
| 18 | +L | (--) | Create new versions of variables (x,y,z). |
| 19 | -L | (--) | Restore the last set of variables. |
| 20 | x! | (n--) | Set local variable X to n. |
| 21 | y! | (n--) | Set local variable Y to n. |
| 22 | z! | (n--) | Set local variable Z to n. |
| 23 | x@ | (--n) | Push local variable X. |
| 24 | y@ | (--n) | Push local variable Y. |
| 25 | z@ | (--n) | Push local variable Z. |
| 26 | x@+ | (--n) | Push local variable X, then increment it. |
| 27 | y@+ | (--n) | Push local variable Y, then increment it. |
| 28 | z@+ | (--n) | Push local variable Z, then increment it. |
| 29 | * | (a b--c) | c = a*b. |
| 30 | + | (a b--c) | c = a+b. |
| 31 | - | (a b--c) | c = a-b. |
| 32 | /mod | (a b--r q) | q = a/b. r = a modulo b. |
| 33 | 1+ | (a--b) | b = a+1. |
| 34 | 1- | (a--b) | b = a-1. |
| 35 | < | (a b--f) | If (a<b) then f = 1 else f = 0. |
| 36 | = | (a b--f) | If (a=b) then f = 1 else f = 0. |
| 37 | > | (a b--f) | If (a>b) then f = 1 else f = 0. |
| 38 | 0= | (n--f) | If (n==0) then f = 1 else f = 0. |
| 39 | min | (a b--c) | If (a < b) c = a else b. |
| 40 | max | (a b--c) | If (a > b) c = a else b. |
| 41 | +! | (n a--) | Add n to the cell at a. |
| 42 | for | (C--) | Start a FOR loop starting at 0. Upper limit is C. |
| 43 | i | (--I) | Push current loop index I. |
| 44 | next | (--) | Increment I. If I < C then jump to loop start. |
| 45 | and | (a b--c) | c = a and b. |
| 46 | or | (a b--c) | c = a or b. |
| 47 | xor | (a b--c) | c = a xor b. |
| 48 | ztype | (a--) | Output null-terminated string a. |
| 49 | find | (--a) | Push the dictionary address a of the next word. |
| 50 | key | (--n) | Push the next keypress n. Wait if necessary. |
| 51 | key? | (--f) | Push 1 if a keypress is available, else 0. |
| 52 | emit | (c--) | Output char c. |
| 53 | fopen | (nm md--fh) | Open file nm using mode md (fh=0 if error). |
| 54 | fclose | (fh--) | Close file fh. Discard TOS. |
| 55 | fread | (a sz fh--n) | Read sz chars from file fh to a. |
| 56 | fwrite | (a sz fh--n) | Write sz chars to file fh from a. |
| 57 | ms | (n--) | Wait/sleep for n milliseconds |
| 58 | timer | (--n) | Push the current system time n. |
| 59 | add-word | (--) | Add the next word to the dictionary. |
| 60 | outer | (str--) | Run the outer interpreter on str. |
| 61 | cmove | (f t n--) | Copy n bytes from f to t. |
| 62 | s-len | (str--n) | Determine the length n of string str. |
| 63 | system | (str--) | Execute system(str). |
| Word | Stack | Description |
|---|---|---|
| version | (--n) | Current version number. |
| output-fp | (--a) | Address of the output file handle. 0 means STDOUT. |
| (h) | (--a) | Address of HERE. |
| (l) | (--a) | Address of LAST. |
| (lsp) | (--a) | Address of the loop stack pointer. |
| lstk | (--a) | Address of the loop stack. |
| (rsp) | (--a) | Address of the return stack pointer. |
| rstk | (--a) | Address of the return stack. |
| (sp) | (--a) | Address of the data stack pointer. |
| stk | (--a) | Address of the data stack. |
| state | (--a) | Address of STATE. |
| base | (--a) | Address of BASE. |
| mem | (--a) | Address of the beginning of the memory area. |
| mem-sz | (--n) | The number of BYTEs in the memory area. |
| >in | (--a) | Address of the text input buffer pointer. |
| cell | (--n) | The size of a CELL in bytes (4 or 8). |
QWC uses a stack-based virtual machine with three main stacks:
- Data Stack: For operands and results (pointers:
dsp,stk). - Return Stack: For return addresses and loop indices (pointers:
rsp,rstk). - Loop Stack: For FOR/NEXT loops (pointers:
lsp,lstk).
Memory is divided into:
- Code Area: Starts at
mem[0], holds compiled code and dictionary. - Dictionary: Grows downward from
mem[MEM_SZ], storing word definitions. - Variables: User variables in the
varsarea.
Instructions are CELL-sized (32/64-bit). Primitives (0-63) execute directly; literals use NaN boxing (64-bit); others are XT calls.
- STATE: 0 (INTERPRET) executes words; 1 (COMPILE) adds them to the dictionary.
- Literals: Small numbers (fitting in 51-bit payload) use NaN boxing; larger ones use
LIT+ value. - Inlining: Copies word definitions up to
EXITfor macros. - Tail-Call Optimization:
EXITafter a call becomes aJMP. - Execution starts with
inner(pc), dispatching via a switch on the opcode.
- Compile: Run
make(requires a C compiler, sets ARCH=64 by default). - Run REPL:
./qwc - Load file:
./qwc filename.fth - Clean:
make clean
Define a word: : square dup * ; (squares TOS).
Loop: 10 for i . next (prints 0 to 9).
Conditional: 5 3 > if 42 . then (prints 42 if true).
| Function | Description |
|---|---|
inner(pc) |
Executes code starting at pc, dispatching primitives/literals/calls. |
outer(src) |
Parses and interprets/executes Forth source string. |
addToDict(w) |
Adds word w to dictionary, returns entry pointer. |
findInDict(w) |
Searches dictionary for word w, returns pointer or 0. |
compileNum(n) |
Compiles number n as literal. |
qwcInit() |
Initializes primitives and built-in words. |
qwc-boot.fth is the bootstrap file loaded on startup, defining higher-level Forth words (e.g., if, begin, variables). It builds on primitives to create a usable language. Edit it to customize the system.
See system.c. It embeds the QWC VM into a C program.
Example usage:
#include "qwc-vm.h"
qwcInit();
outer("your forth code here");