Forth is perhaps the tiniest possible useful interactive programming language. It is tiny along a number of dimensions:
- The amount of code required to implement it
- The size of the code that is generated
- The amount of memory used
- The number of features it considers necessary for useful work
It is a language that makes complexity painful, but which reveals that a surprising amount can be accomplished without introducing any. Forth is the opposite of “bloat”. If you've ever been like “Oh my God this Electron-based chat app is taking up 10% of my CPU at idle, what the HELL is it DOING, modern computing has gone MAD”, Forth is there to tell you that computing went mad decades ago, and that programs could be doing SO MUCH MORE with SO MUCH LESS.
WHAT DO YOU MEAN, “FORTH”
There is an expression about Forth: “If you've seen one Forth, you've seen one Forth.” Forth isn't a strictly-defined language, though there is a standardized dialect; it's more a set of ideas that tend to work well together.
In the past month, I wrote a tiny Forth system on a 286 running MS-DOS using Turbo C++ 1.01. It is my first time using Forth in anger, though I read a lot about it 15 years ago. When I refer to my Forth, I am referring to a system literally thrown together in two weeks, written by someone who does not really know Forth that well. It is slow and wildly nonstandard and it doesn't do very much, but I have enjoyed the process of writing it very much. If you are a grizzled old Forth grognard, please let me know if I have misrepresented anything.
WHAT DOES FORTH NOT DO
Here is an incomplete list of things you may take for granted as a programmer that Forth, in its purest form, generally considers unnecessary waste:
- Garbage collection
- Dynamic memory allocation
- Memory safety
- Static types
- Dynamic types
- Polymorphic methods
- Lexical scoping
- The concept of global variables being in any way “bad”
- Local variables
- The ability to write “IF” statements at the REPL
Most or all of these can be added to the language – the Forth standard, ANS Forth, specifies words for dynamic memory allocation and local variables. There are lots of object systems that people have built on top of Forth. Forth is a flexible medium, if you're willing to put in the work.
But the inventor of Forth, Chuck Moore, literally said, in 1999: “I remain adamant that local variables are not only useless, they are harmful.” In the Forth philosophy, needing to use local variables is a sign that you have not simplified the problem enough; that you should restructure things so that the meaning is clear without them.
WHAT DOES FORTH LOOK LIKE
A core part of Forth is that all functions, or “words” in Forth terminology, operate on “the stack”. Words take arguments from the stack, and return their results on the stack. There are a handful of primitive built-in words that do no useful work besides manipulating the stack.
What this means is that writing an expression tree as Forth code ends up turning into postfix notation.
(1 + 2) * (3 - 4) becomes
1 2 + 3 4 - *. Writing a number in Forth means “push that number onto the stack”.
Forth syntax is, with a few exceptions, radically, stupefyingly simple: Everything that's not whitespace is a word. Once the interpreter has found a word, it looks it up in the global dictionary, and if it has an entry, it executes it. If it doesn't have an entry, the interpreter tries to parse it as a number; if that works, it pushes that number on the stack. If it's not a number either, it prints out an error and pushes on.
Oops, I meant to describe the syntax but instead I wrote down the entire interpreter semantics, because it fits in three sentences.
The exception to the “whatever is not whitespace is a word” rule is that the interpreter is not the only piece of Forth code that can consume input. For example,
( is a word that reads input and discards it until it finds a
) character. That's how comments work – the interpreter sees the
( with a space after it, runs the word, and then the next character it looks at is after the comment has ended. You can trivially define
( in one line of Forth.
WHY THE HELL WOULD I USE THAT
There are practical reasons:
- You need something tiny and reasonably powerful, and you don't care about memory safety
- I'm not sure I can think of any others
And there are intangible reasons:
- Implementing a programming language that fits into a few kilobytes of RAM, that you understand every line of, that you can build one piece at a time and extend infinitely, makes you feel like a god-damn all-powerful wizard
Part of the mystique of Forth is that you can get very metacircular with it – control flow words like IF and FOR are implemented in Forth, not part of the compiler/interpreter. So are comments, and string literals. The compiler/interpreter itself is usually, in some way, written in Forth. It turns out that you can discard virtually every creature comfort of modern programming and still end up with a useful language that is extensible in whatever direction you choose to put effort into.
Forth enters that rarefied pantheon of languages where the interpreter is, like, half a page of code, written in itself. In many ways it's kind of like a weird backwards lisp with no parentheses. And it can be made to run on the tiniest hardware!
The mental model for bootstrapping a Forth system goes something like:
- Write primitive words in assembly – this includes the complete Forth “VM”, as distinct from the Forth language interpreter/compiler. The set of built-in words can be very, very small – in the document “eForth Overview” by C. H. Ting, which I have seen recommended as an excellent deep-dive into the details of how to build a Forth environment, Ting states that his system is built with 31 “primitive” words written in assembly.
- Hand-assemble “VM bytecode” for the interpreter/compiler and required dependencies – because of the extreme simplicity of the VM, you can generally program your macro assembler to do this job, and so this can meaningfully resemble the act of simply writing Forth code directly
- Write all new words using the interpreter/compiler you just got running
I say “interpreter/compiler” and not “interpreter and compiler” because they are literally mixed together; there is a global flag that determines whether the interpreter is in “compile mode” or not. It is done this way because it turns out that if you add the ability to mark a word as “always interpret, even in compile mode”, you have added the ability to extend the compiler in arbitrary ways.
WHAT SUCKS ABOUT WRITING FORTH
Any word that takes more than two or three parameters is a nightmare to read or write
Right now in my codebase I have a word that uses two global variables because I cannot deal with juggling all of the values on the stack. This word is absolutely not re-entrant and at some point I'm going to need to rewrite it so that it is, and I am not looking forward to it. If I had local variables, it would be substantially less of a problem. But there's also part of me that thinks there must be some way to rewrite it to be simpler that I haven't figured out yet.
There's another word in my codebase that takes 4 or 5 parameters that I managed to write by breaking it up into, like, 8 smaller words, over the course of writing / rewriting for like an hour or two. I felt pretty proud when I finally got it working, but honestly I think it would have been pretty trivial to write in C with local variables. I miss them.
Remember the part about no memory safety? Yeah, there's all kinds of ways a wayward Forth system can go wrong. I forgot a
DROP once in a frequently-used word and my computer hard-locked when the stack overflowed. (To be fair: my computer was a 286 running MS-DOS, so I was already in a situation where programming it meant rebooting it when I inevitably fucked something up.)
Nonexistent error messages
The only error message my Forth system has is, if it doesn't recognize the word “foo”, it prints “foo?” If, for example, I write an
IF statement, but forget to end it with
THEN, I don't get a compile error, I get — you guessed it — a runtime hard crash.
WHAT RULES ABOUT WRITING FORTH
It's compact as hell
The majority of words I write are literally one line of code. They do a small job and get out.
It's direct as hell
Building abstractions in Forth is... different than building abstractions in other languages. It's still a really core, important thing, but as building complex / expensive code is so much work, stacking expensive abstractions on top of each other is not really tenable. So you're left with very basic building blocks to do your job as straightforwardly as possible.
You are absolutely empowered to fix any problems with your particular workflow and environment
People turn Forth systems into tiny OSes, complete with text editors, and I absolutely did not understand this impulse until I wrote my own. The Forth interpreter is an interactive commandline, and you can absolutely make it your own. Early on I wrote a decompiler, because it was easy. It's like half a screen of code. There are some cases it falls down on, but I wrote it in like a half hour and it works well enough for what I need.
Everything is tiny and easy to change or extend
Remember when I said I wrote a decompiler because it was easy? Other things I changed in an evening or two:
- Added co-operative multitasking (green threads)
- Custom I/O overrides, so my interactive REPL sessions could be saved to disk
- Rewrote the core interpreter loop in Forth
- Rewrote the VM loop to not use the C stack
- Instrumenting the VM with debug output to catch a crash bug
One of the things on my todo list is a basic interactive step-through debugger, which I suspect I'll be able to get basically up and running within, like, an hour or two? When things stay tiny and simple, you don't worry too much about changing them to make them better, you just do it.
If you have ever wanted an assembly code REPL, this is about as close as you're going to get
Forth is a dynamic language in which the only type is “a 16-bit number” and you can do whatever the fuck you want with that number. This is dangerous as hell, of course, but if you are writing code that has no chance of having to handle arbitrary adversarial input from the internet (like my aforementioned MS-DOS 286), it is surprising how refreshing and fun this is.
THIS SOUNDS INTERESTING, WHAT IS THE BEST WAY TO LEARN MORE
I honestly do not know if there is a better way to understand Forth than just trying to build your own, and referring to other Forth implementations and documents when you get stuck. It's been my experience that they just don't make sense until you're neck deep into it. And it's tiny enough that you feel good about throwing away pieces that aren't working once you understand what does work.
I've found the process of writing my own Forth and working within its constraints to be far more rewarding than any time I have tried working with existing Forths, even if on occasion I have wished for more complex functionality than I'm willing to build on my own.
WHAT HAVE I LEARNED FROM ALL THIS
I'm very interested in alternate visions of what computing can look like, and who it can be for. Forth has some very interesting ideas embedded in it:
- A system does not have to be complex to be flexible, extensible, and customizable
- A single person should be able to understand a computing system in its entirety, so that they can change it to fit their needs
I find myself wondering a lot what a more accessible Forth might look like; are there more flexible, composable, simple abstractions like the Forth “word” out there? Our current GUI paradigms can't be irreducible in complexity; is there a radically simpler alternative that empowers individuals? What else could an individual-scale programming language look like, that is not only designed to enable simplicity, but to outright disallow complexity?
Forth is a radical language because it does not “scale up”; you cannot build a huge system in it that no one person understands and expect it to work. Most systems I have used that don't scale up – Klik & Play, Hypercard, Scratch, that sort of thing – are designed for accessibility. Forth is not; it's designed for leverage. That's an interesting design space I wasn't even really aware of.
The lesson that implementing abstractions as directly as possible enables you to more easily change them is a useful one. And the experience of succeeding in building a programming environment from scratch on an underpowered computer in a couple of weeks is something I will bring with me to other stalled projects – you can sit down for a couple of hours, radically simplify, make progress, and learn.