Collapse OS — Why Forth?

Collapse OS' first incarnation was written in Z80 assembler. One of the first feedbacks I had after it went viral was "why not Forth?". I briefly looked at it and it didn't seem such a great choice at first, so I first dismissed it. Then, I had what alcoholics refer to as a "Moment of clarity".

Forth is a stellar fit to Collapse OS design goals. If you're not familiar with it, it might be hard to understand why. Let me try to explain.

What it's not

Some Forth enthusiasts like the language for itself. They think that its easier to express much of the logic of a program in this language. Many books and tutorials are touting this.

That's not my opinion. Forth is a fine and capable language, but I tend to agree with those who say that it tends to produce write-only programs. Most of the books on Forth are very old and compare Forth's elegance to Fortran's or COBOL's. I'm of the opinion that C is more expressive and generally produces more elegant and maintainable code.

When you look for Forth's advantages, don't look there.

Forth isn't particularly fast either. As an interpreted language, it is blazing fast, but the speed tax of the threaded model still has to be paid, so it will generally be slower than code produced by a C compiler.

However, as a Python developer, I know this tradeoff well. It's not 100% of your program that needs to be fast, only bottlenecks. Like with Python, it's easy in Forth to write bottlenecks in native code. Therefore, speed is not such a big issue.

What it is

Forth is, to my knowledge, the most compact language allowing high level constructs. It is so compact that Collapse OS' Forth implementation achieves self-hosting with about the same amount of resources than its assembler counterpart!

What is this sorcery? At face value, nothing can be more compact than native code. It's self-evident. If a piece of Forth code is doing the exact same piece of logic in a way that is as compact as native code, you've been lazy on your assembly writing!

To understand this compactness, we have to look deeper.

The Z80 asm version of Collapse OS self-hosts on a RC2014 with a 5K shell on ROM, a 5K assembler binary loaded in RAM from SD card (but that could be in ROM, that's why I count it as ROM in my project's feature highlights) and 8K of RAM. That is, it can assemble itself from source within those resources.

The biggest piece of software to assemble is the assembler itself. It's a reasonably well-featured assembler that supports forward labels, includes, many useful directives. The code implementing those features requires those aforementioned resources to assemble.

...But also, the features included in the assembler are required to assemble its source code! I could cut a bit on its features to reduce resource usage, but I'd need to adapt the assembler's code for this lack of features and possibly make the code much more complex. I believe that with this assembler, I stroke a good balance and achieved something close to optimal.

If I wanted to re-implement that assembler feature-for-feature in Forth, it would probably require much more resources to build. Even though higher level words are more compact, the base of the pyramid to get there couldn't compete with the straight assembler version. This was under this reasoning that I first dismissed Forth.

So, again, what makes Forth more compact than assembler? Simplicity. The particularity of Forth is that it begins "walking by itself", that is, implementing its own words from its base set, very, very early. This means that only a tiny part of it needs to be assembled into native code. This tiny part of native code requires much less tooling, and thus an assembler with much less features. This assembler requires less RAM.

What is more compact than something that doesn't exist? Even Z80 assembler can't beat the void.

That's how although Forth is not more compact than native code (duh!), a Forth Collapse OS achieves self-hosting with as much resources than its Z80 counterpart.

So for the very low price of manageable speed constraints, we get a high level language that is much more powerful and extensible than the BASIC shell that was previously there. I call that a definite win.

Cherry on top

Because compactness has always been a primary design goal of Collapse OS, I initially chose Z80 assembler over high level languages so that I could continue to meet that design goal.

However, now that I can have my cake and eat it too, thanks to Forth, new opportunities open. First and foremost, multiple architectures! Although I was ready to give that up when I tied myself to the Z80, I'm now very glad to have it and will certainly take advantage of it.

Note: This text was written shortly after Collapse OS' Forth rewrite, before it started supporting multiple architectures. This is why this "cherry on top" is evoked as something to reach for. Collapse OS has been running on multiple CPUs for a while now.

Culture of simplicity

Forth's culture of simplicity is worth writing about. At face value, a traditional Forth environment is severly constrained according to modern standards.

Who would want to work within the constraint of 64x16 blocks filesystem? Someone who knows the complexity associated with implementing a filesystem and everything that underpins it. When simplicity is valued, such constraints are gladly accepted.

We can't say that asceticism is a dominant trait of our relation to technology. We aren't used to willingly accept constraints with regards to what we could call our convenience. Why would anyone not want a piece of software or hardware to be easier to use, more accessible to the layman, more featureful? If someone is willing to build that complexity, Moore's law allow us to adopt it, and we do. This leads to cancerous complexity, layers upon layers upon layers with each new generation of developers blissfully accepting the previous layers.

(Something analogous can probably be said about our collapsing civilization, and probably even eloquently quote Joseph Tainter)

The base design of Forth is entirely built around a primal simplicity. Even better, the path of least resistance generally guides you towards simplicity. When a word definition becomes too big and complicated, it generally prompts you to reorganize your code toward a simpler solution that might not have occurred to you in another language.

To be clear: Forth doesn't elegantly describe complex algorithms, at least not when compared to languages like C. Those generally make the head hurt when reading. However, this mental pain does makes you question your need for complexity and encourages "sideways" solutions.

This is something that I think few developers are familiar with and is hard to describe with words. It needs to be experienced. One of the good ways to experience it I think is to implement your own Forth.

Addendum: Forth's readability

When I wrote the text above over a year ago, I had successfully implemented my own Z80 Forth, but I couldn't consider myself a competent Forth programmer yet. Now I think that I can and that I've done enough Forth to speak about readability in a bit more details.

Forth is hard to read. Maybe you can create abstractions to make it easier to read, but it's going to cost you in terms of simplicity and at some point it becomes self-defeating: maybe the language you're after isn't Forth.

I think it's inherent to Forth, reading it simply requires a much higher cognitive load because you have to hold a lot more information into your head as you read along. It's exhausting for the mind (but you get better at it after a while). It is also slower. Accept it or you're going to have a bad time.

This doesn't mean, however, that working within an existing Forth codebase is significantly harder than with other languages: simplicity means you have a lot less code to manage. At least it doesn't have to be (any programmer can create an unworkable mess in any language, you know that).

What I think is key to keep a codebase workable is to, at all times, favor simplicity. When the meaning of a word is obvious, you don't need to read its content, so who cares if its code is "write only"? Rewrite it if you're unhappy with it, it's most likely a one-liner anyway.

With experience, I find that I rarely read my past code. I spend more time making a mental inventory of words I'm going to use for the words I'm about to write. This is different from reading code.

When I do read code, I admit that it's difficult, but most of the time, I only read it to make sure that I understand what the word was doing before I re-write it entirely. With experience, I end up relying a lot more on my own documentation (word listings with signatures and descriptions) instead of relying on re-reading my code, as I have the habit of doing when working in other languages.

Reading Forth is slower, but writing it doesn't have to be. However, if your mind is all excited to reach the solution you're thinking about and you're writing it rapidly, it usually means words with fuzzy meanings, which is hard to maintain. I have to quote Yoda here.

How do I know I'm writing good Forth code, Virgil? When you are calm, at
peace, passive.

I'm throwing an open question into the wind: Maybe readability is overrated?

One thing I can confirm, however, is that Forth's path of least resistance is simplicity and this is awesome. It requires more discipline, some brain training to handle cognitive load, but the more I do it, the more I like it.

Addendum 2: Tumble Forth

My love letter to Forth continues in my Tumble Forth blog.