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:

  • rsi is the Forth instruction pointer, walking through a dictionary word's "thread" (a list of execution tokens)
  • rbp the data (parameter) stack pointer
  • rsp the native CPU stack, reused directly as the Forth return stack: colon-word calls push/pop it via DOCOL/EXIT just like a normal call stack. DO...LOOP keeps 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" (pushes addr len on 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