Two Paths to a Forth
Forth is one of those languages that tends to sit in the back of a programmer's mind labeled "weird" — until something forces a closer look. For me, that push came from two directions: an itch to write code after months of theory-heavy work, and reading Dave Gauer's write-ups on Forth and its implementation.
What became clear quickly is that Forth is best understood at two levels. At the user level, it's a practical stack-based calculator and REPL, useful for scripting hardware or interactive tasks. At the hacker level, it's a fascinating exercise in self-extension: even control flow words like IF...THEN and BEGIN...UNTIL are themselves Forth words. If you want, you can define your own control flow constructs that work just as seamlessly as the built-in ones. A useful analogy: user-level Forth is like Lisp without macros; hacker-level Forth has macros enabled.
That distinction drove my two implementations. The first, goforth (the Go version in the repository root), is deliberately a pure interpreter. Word definitions are stored as source strings and interpreted on invocation. This works fine for user-level Forth — it's usable, if slow — but it hits a wall at the hacker level. Because the Go interpreter holds all the control, words like IF...THEN can't be written in Forth itself; they have to live in the host language.
The second implementation, ctil, takes the classic route: a threaded, linked dictionary in C, inspired by jonesforth. This is the architecture Forth was originally designed around, and it's what makes hacker-level Forth possible. In ctil, significant parts of Forth are implemented in Forth itself, loaded from a "prelude" file before user code runs. variable, for instance:
: variable create 1 cells allot ;
And conditionals:
\ IF, ELSE, THEN work together to compile to lower-level branches. \ \ IF ... THEN compiles to: \ 0BRANCH OFFSET true-part rest \ where OFFSET is the offset of rest \ \ IF ... ELSE ... THEN compiles to : \ 0BRANCH OFFSET true-part BRANCH OFFSET2 false-part rest \ where OFFSET is the offset of false-part and OFFSET2 is the offset of rest : if immediate ' 0branch , here 0 , ; : then immediate dup here swap - swap ! ; : else immediate ' branch , here 0 , swap dup here swap - swap ! ;
This is where Forth gets genuinely impressive. Words like IF work by laying out their low-level representation directly in memory during compilation, and words communicate with each other through the data stack while compiling. It's a completely different model from what most programmers are used to.
Why Forth Made Sense — Then and Now
Forth's design makes perfect sense in its historical context. In the early 1970s, if you had hardware to control and no C compiler (or a language like BASIC) to hand, Forth was a practical gift. It's simple to implement in assembly, yet gives you a far higher-level language than raw assembly: a calculator, a REPL, and a composable DSL for almost anything.
As a language, Forth is concatenative and inherently point-free. Where you might traditionally write:
eat(bake(prove(mix(ingredients))))
Forth lets you write:
ingredients mix prove bake eat
Parameters and return values never appear explicitly — everything flows through the stack. That's excellent for interactive, REPL-style work with hardware, and it's also what keeps Forth's implementation so lean.
The Readability Problem
But that same terseness is Forth's biggest modern weakness. It's hard to read at scale. Consider a standard word like +!, which adds a value to the contents of an address. An implementation from ctil's prelude:
: +! ( addend addr -- ) tuck ( addr addend addr ) @ ( addr addend value-at-addr ) + ( addr updated-value ) swap ( updated-value addr ) ! ;
Without the stack-effect comments on each line (a common Forth idiom), this is nearly indecipherable. Compare that with straightforward C code:
int func(int a, int b) {
return foo(a, bar(b));
}
There's no ambiguity: bar takes one parameter and returns one value; foo and func each take two parameters and return one; data flow between calls is obvious. The same logic in Forth:
: func bar foo ;
You can't tell function arity or value flow without comments. In a small, well-known vocabulary, that's fine. In a sprawling, unfamiliar codebase, it becomes a serious obstacle.
Forth is instructive and rewarding to implement, but I'd hesitate to reach for it in real work today. It's firmly in the "weird language" category. The stack model is elegant in small doses, but for general-purpose programming, readability and reasoning suffer.
Resources and Takeaways
Both implementations live in the goforth repository, complete with a shared test harness. For learning for Forth itself, two resources stood out:
- Dave Gauer's Forth page
- Starting Forth — a free online tutorial
For understanding the implementation side:
- Dave Gauer's notes on implementing Forth
- Richard W.M. Jones's jonesforth
- Lehrhaupt's Threaded Interpretive Languages, a dated but still excellent book on how Forth implementations work
Implementing Forth is genuinely worth the effort. It has a challenging conceptual hump, but once you're over it, you'll have a visceral understanding of stack machines, the boundary between interpreting and compiling, and how these two layers can be mixed. Plus, there's real joy in interacting with your own language implementation — and then just adding one more word to it.



