Revisiting Forth after 40 years for Advent of Code 2025
For the 2025 Advent of Code I decided to solve the puzzles in Forth.
I learnt Forth back in 1983 or so to program 8 bit computers (my ZX81 with an alternate ROM) and used it for a few years, so I thought it would be fun to try it again 40 years later in 2025 and to refresh my knowledge of Forth to modern standards. I’ve forgotten a lot of things about Forth but I remember the joy of crafting the perfect word for a problem.
I use Linux for my day to day computing needs, so I chose the most popular Forth on that platform which is Gforth.
From the website:
Gforth is a free, fast, featureful and portable implementation of the Forth 2012 language.
Looks great!
W…
Revisiting Forth after 40 years for Advent of Code 2025
For the 2025 Advent of Code I decided to solve the puzzles in Forth.
I learnt Forth back in 1983 or so to program 8 bit computers (my ZX81 with an alternate ROM) and used it for a few years, so I thought it would be fun to try it again 40 years later in 2025 and to refresh my knowledge of Forth to modern standards. I’ve forgotten a lot of things about Forth but I remember the joy of crafting the perfect word for a problem.
I use Linux for my day to day computing needs, so I chose the most popular Forth on that platform which is Gforth.
From the website:
Gforth is a free, fast, featureful and portable implementation of the Forth 2012 language.
Looks great!
What is Forth?
Forth is a stack based language with minimal syntax. It is based on "words" which read and write things to a stack. Word names can contain any characters and are separated by white-space. That is about all the syntax you get!
Forth has two modes, interpreting and defining. Interpreting looks like this
1 2 3 * + .
What happens here is that the interpreter recognises a number and pushes its value onto the stack. The other symbols are words. * multiplies the top two things on the stack and returns the answer to the stack. + is similar but adds. . takes the top value from the stack and outputs it. Thus when you press enter this prints the result of 1+(2*3)
7 ok
The : word switches Forth into defining mode to make new words and the ; switches out. Here is a word which doubles what is on the stack
: double 2 * ;
Comments are enclosed in ( and ). These are often used for stack comments showing either what is on the stack now or the stack effect of a word:
: square ( a -- a^2 )
dup *
;
Some words (known as immediate words) run during compile time. These are used to implement control flow. For example do and loop
: squares ( -- )
10 0 do
I . ." squared is "
I square . cr
loop
;
You can of course make your own immediate words and hence your own control flow words. Because you can run code at compile time you can make Forth do pretty much whatever you want. The modern version of this might be Zig’s comptime.
Forth is fun because it is so minimal and so flexible. The stack based notation gets a bit of getting used to but gets out the way quite quickly. It is also really easy to implement a Forth from scratch. An excellent example to look at is jonesforth which bootstraps a minimal Forth from x86 assembly.
If you want to learn more check out the Gforth tutorial. I learnt Forth from Leo Brodie’s excellent books Starting Forth and Thinking Forth. In particular Thinking Forth influenced me a lot as a programmer teaching me how to break problems down into manageable pieces. It has very funny cartoons too, some of which I still think about today.
Forth has changed
Forth has evolved since I learnt it in 1985. On my 64 bit Linux machine cells on the stack are 64 bits rather than the 16 bits they were in 1985.
A lot has stayed the same though.
Probably the biggest change is local variables. Old Forth did not have local variables, if you wanted a variable in a word you would keep them on the stack which meant juggling things around continuously.
For a simple example, lets say you had a word to calculate the hypotenuse of a triangle sqrt(a^2+b^2). You would write it like this with copious stack comments so you didn’t get confused about what was where on the stack.
: hypotenuse ( a b -- c )
dup * ( a b^2 )
swap ( b^2 a )
dup * ( b^2 a^2 )
+ ( b^2+a^2 )
sqrt
;
But with local variables it turns out like this (these are Gforth local variables) which is much easier to read and no stack comments needed:
: hypotenuse { a b -- c }
a a * b b * + sqrt
;
The { word defines the local variables a and b and initialises them with what is on the stack. Everything after the -- is a comment used to document the stack effect of the word until the closing }.
Local variables get rid of most of the stack manipulation words (eg dup, swap, over) and most of the stack comments which I think is a positive. Forth programs have often been criticised for "stack noise" (the stack manipulation words which don’t actually contribute to the code) so fewer is better here, definitely.
There are lots of other innovations like:
valuefor mostly read only variables withto varto write them. This gets rid of a surprising amount of@and!noise and is a great improvement onvariable.- ability to define structures
- use of dynamic memory (eg
mallocandfree) - use of files (no we didn’t have those in 1985 we had these weird things called buffers which came off tape!)
- exceptions
- floating point (on a separate stack which is a bit weird on a 64 bit machine but, oh well)
- much better dictionary support (think namespaces)
- standardisation with Forth 2012 - Forth was not well standardised in 1985, programs weren’t portable between different computers or Forths.
I think Gforth is significantly easier to program than old school Forth. I like it!
The ugly parts
Coming back to Forth there are some really ugly parts.
Type safety
No type safety at all. Feel free to mix up pointers and integers, overwrite buffers etc. Believe it or not C is a much safer language than Forth! I knew Forth was like this of course but maybe I’ve been spoilt with so many years of languages which have a type system!
At least Gforth shows you a sensible backtrace when you mess up rather than just resetting your computer (when you messed up the return stack) like it used to in the bad old days!
Looping
The standard looping constructs are not ideal.
There are begin again for an infinite loop, begin while repeat for a generalised while loop and do loop for a counted loop. So far so good, but:
-
you can use
leaveto early exit (break) adoloop -
there is no way to early exit a
beginloop! -
if you use
exit(equivalent ofreturnin C/Python) to leave the word in adoloopyou need to remember to callunloopfirst otherwise the program crashes! -
none of these loops has a
continuewhich is maddening -
0 0 do loopdoesn’t loop zero times like you might expect, it loops 2^64 times (on 64 bit machines) which isn’t very useful. -
I eventually learned that you can use
?doinstead ofdoto avoid this.
Forth being Forth you can define your own looping constructs of course so I should do that and quit complaining :-)
Speed
Gforth is really slow. Forths needn’t be slow - they can compile to native code - so I think this is Gforth’s problem. I got Gemini to do a line by line conversion of the puzzle3a.fth to C and the C code ran 25 times quicker! (37s down to 1.5s) (My son’s solution, which he also wrote in C, ran in 2ms so I don’t think I had the best algorithm!)
Strings
Parsing strings really isn’t Forth’s strong point. The fact that strings are counted in Forth so occupy two spots on the stack ( addr length ) make using them really long winded and there are very few words in the standard library which do useful stuff to strings. It is tedious having to write "find this character in a string but don’t overrun the end" code and "split this string into other strings".
I did by the end have a set of words that parsed the AoC input without too much pain though which I adapted from one day to the next.
Conclusion
These are my reflections on using Forth in 2025. I’m sure any other Forth programmer will disagree with at least one thing I say, so please take this as personal opinion only.
I enjoyed refreshing my Forth knowledge. I discovered I still enjoy programming in Forth and even more so in modern Forth. I’m unlikely to release code written in Gforth though as it doesn’t have a compiler so has all the distribution problems Python has.
Parsing the puzzle input was hard work, as was building data structures. C structs are a lot easier to use than Forth ones and the member names are namespaced which is helpful.
Once the input was parsed, solving the problems just was like using any other language. Because Forth is a bit harder to write it made iterating the solutions harder - that was definitely a barrier. The Forth REPL was useful but I used it much less than I did before - perhaps because I’ve got used to editing the code in emacs then running it. I did build the code bottom up in small segments which is very Forth and also my natural style (perhaps because of my early Forth exposure).
Debugging was done with print statements (as usual) and backtraces. The backtraces pinpointed the word that crashed but not the line of code. This is less of a problem than you might think since Forth words tend to be quite short.
I found programming Forth easier than programming in assembly but harder than programming in C. Forth has about the same type safety as assembly (None!). If only Gforth was as fast as assembly!
I definitely got some of the joy of crafting the perfect word for the job while doing these puzzles. Forth encourages you to whittle things down the minimum and doing that is part of the fun of coding for me.
It was a challenging, nostalgic and rewarding way to tackle Advent of Code; perhaps this vintage language still has some life left! If you are looking to shake up your programming routine, I highly recommend giving Forth a try for something completely different.