forth
August 8, 2026 ยท View on GitHub
A small Forth interpreter for Linux/amd64, written in pure NASM assembly without any external dependencies.
Compile via the included Makefile:
$ make
nasm -f elf64 forth.asm
ld -o forth forth.o
Usage
Forth reads a program from STDIN, you can see example.forth for a complete example, and run it via:
$ make run
./forth < example.forth
..
Showing factorials
0 0
1 1
2 2
3 6
4 24
5 120
6 720
..
make test runs the same file and diffs its output against the known-good result example.expected.
For interactive usage just launch the interpreter and enter code into the REPL:
$ ./forth
There is no prompt, just enter your program(s), and exit with BYE or Ctrl-D.
The contents of the file forth.inline are embedded in the binary and executed before any user-input, we could define more words there as part of a bundled standard-library.
Architecture
Classic indirect-threaded code, the same design used by minimal Forths such as jonesforth:
rsiis the Forth instruction pointer, walking through a dictionary word's "thread" (a list of execution tokens)rbpthe data (parameter) stack pointerrspthe native CPU stack, reused directly as the Forth return stack: colon-word calls push/pop it viaDOCOL/EXITjust like a normal call stack.DO...LOOPkeeps its limit/index pairs on a separate loop-control stack instead (see "Known limitations" below), so it's unaffected by calls into other words
See the comment block at the top of forth.asm for the full dictionary entry layout and the NEXT/DOCOL mechanics.
Words
We've not implement vary many words, but we have the basics
- Stack:
DUP DROP SWAP OVER ROT 2DUP 2DROP .S ?DUP DEPTH PICK - Return stack:
>R R> R@ - Integer arithmetic:
+ - * / MOD NEGATE 1+ 1- ABS MIN MAX - Comparisions (true = -1, false = 0):
= < > 0= 0< - Logic:
AND OR INVERT - Memory set/get:
! @ - I/O:
. EMIT CR KEY - Defining words:
: ; IMMEDIATE VARIABLE CONSTANT - Conditionals:
IF ELSE THEN - Loops:
BEGIN UNTIL,DO LOOP I J - Strings:
." text"(prints immediately, or at run time if compiled).S" text"(pushesaddr lenon the data stack),TYPE(addr len --, prints)
- Comments:
\runs to end of line - Other:
BYE(exit)
You can see all defined words by running WORDS.
Special quirks? A 3-character word of the form 'x' (a single quote, any one byte, a single quote) is a character literal and pushes that byte's value, e.g. '*' pushes 42.
Example
There's an example in example.forth, but here's a small extract:
: square dup * ;
: fact dup 1 > if dup 1 - fact * then ;
5 square . \ 25
6 fact . \ 720
Anti-features
- No floating point, integers only.
- String literals are capped at 255 characters.
- Error handling is a bit adhoc, testing for stack exhaustion is mostly done but it's all a bit random.
- Having separate stacks is probably a mistake but it was easier to reason about.
See Also
- FORTH-like language, implemented in a tutorial-style in golang