hckrnws
The FORTH code for Chipwits is released in the game's 40th anniversary
by JoeDaDude
Here's a post I wrote about why I used FORTH to code ChipWits in 84:
https://chipwits.com/2023/04/08/forth-programming-language-g...
Forth has been something I've wanted to learn for years now. It seems weird to me that for most stuff in old computers, you have the option of "assembly" if you want your program to be fast, and "BASIC" if you want your program to be slow, but Forth lingers along as the "medium speed" language, despite at least looking pretty high-level.
Forth was the standard language for hardware development and drivers (along wiht C) for a very long time (still is used all over). Stack based.
The reason I used FORTH to code ChipWits in 84 was twofold. First, it allowed me to develop natively on the 128k Mac rather than buying an outrageously expensive Lisa. Second, I knew I was going to port it to other micros and FORTH was usually one of the first languages implemented on new computers. I eventually ported it to Apple II and C64 and about 70% of the Mac code was easily portable.
Forth really is one of easiest languages to build up from bare metal, piece by piece. And when you get it working, sure, it's arguably weird, but it's far better than where you started.
My personal inclination is to make the longer jump, and go straight for a deeply rudimentary Lisp. There's a trick where you start off with Lisp macros that expand to assembly, and I once knew someone who got it working for new hardware during a 10-hour plane flight. It's a slightly longer climb than Forth, but even a primitive Lisp is nice.
However, the deciding factor here really is the 6502 and 65C02 microprocessers. You really want at least 4 general-purpose registers for a primitive Lisp dialect, and that's pushing it. And the 65C02 basically has 1, or 3 if you clap your hands and believe. Even C is a serious challenge on a 65C02.
But Forth thrives in that enviroment. You only need a stack pointer and enough registers to do exactly 1 canned operation. So: victory to Forth.
And wow, I wish I had seen Chipwits back in the day. I was a massive fan of the Rocky's Boots logic game, but Chipwits never showed up in our neck of the woods. Thank you for open sourcing it!
Do you have any texts/websites/papers that would allow one (me) to learn about "deeply rudimentary Lisp" and how to create one? I am especially interested in learning why 4 general-purpose registers are important and other lower-level details like that.
Sure! One fantastic starting point is Lisp in Small Pieces, which shows you how to build multiple different Lisp interpreters, and then several increasingly fancy Lisp compilers.
The trick with a macro-assembler that uses Lisp macros to generate assembly was basically folklore when I learned it, and I haven't seen it fully fleshed out anywhere in the literature. For a tiny chip, you'd run this as a cross compiler from a bigger machine. But you basically have Lisp macros that expand to other Lisp macros that eventually expand to assembly representated as s-expressions.
As for why basic Lisps are register-hungry, you usually reserve an "environment pointer register", which points to closure data or scope data associated with the currently running function. And then you might also want a "symbol table base" register, which points to interned symbols. The first symbol value (located directly where the symbol register points) should be 'nil', which is both "false" and the "empty list". This allows you to check Boolean expressions and check for the empty list with a single register-to-register comparison, and it makes checks against other built-in symbols much cheaper. So now you've sacrificed 2 registers to the Lisp gods. If you have 8 registers, this is fine. If you have 4 registers, it's going to hurt but you can do it. If you have something like the 65C02, which has an 8-bit accumulator and two sort-of-flexible index registers, you're going to have to get ridiculously clever.
Of course, working at this level is a bit like using #[no_std] in Rust. You won't have garbage collection yet, and you may not even have a memory allocator until you write one. There are a bunch of Lisp bootstrapping dialects out there with names like "pre-Scheme" if you want to get a feel for this.
Forth is a stack machine, so you basically just need a stack pointer, and a couple of registers that can be used to implement basic operations.
Anyway, Lisp in Small Pieces is fantastic, and it contains a ton of the old tricks and tradeoffs.
I heartily endorse Lisp In Small Pieces. It's sitting beside me right now.
I recently wrote an assembler in scheme; I'm in the process of adding macros. You need very few primitives to implement what amounts to a lisp compiler. A larger issue is bootstrapping garbage collection from manual memory allocation—while there are a few tricks to do this simply, if inefficiently, high-performance garbage collection needs to be tightly integrated with the compiler in order to implement a) scanning for roots, b) rewriting pointers when you move allocations around, and c) likely pointer tagging. None of this is easy to design around or to bolt on to a macro-oriented compiler-assembler.
And of course, writing the really fancy bits of lisp—escaping closures, continuations, conditions—take a lot more effort and thought and care.
Curiously, garbage collection is far from necessary to achieve bootstrapping. It's just rather odd to use a lisp with manual memory allocation. I've found stealing from Rust's memory model has been very fruitful in this regard (not the borrow checker). RAII is also very easy to implement with careful attention to how scoping and moving is implemented.
Thank you! This is wonderful.
Thanks for opening the source code to another generation
Yeah I knew that, that's why I've found it interesting.
It looks a lot more high-level than something like C, but is used in similar spaces, which makes me wonder why it never really became more popular.
It may look more high-level than something like C, but it is actually no more high level than a macro assembler with registers eliminated. As there's no syntax tree, essentially every word that is parsed from the input stream is replaced with a subroutine call. So the resulting FORTH code is nothing but a series of subroutine calls.
In my experience quite often writing in assembler is easier than FORTH unless you have a strategy and self discipline, which when acquired makes one a whole lot more productive than using assembler, and arguably more so than a high level language. There're no pointer arithmetics, no rudimentary type checking, not even an array type (you have cells and addresses). There is nothing that throws an error except things like stack over/under-flow checks, and if you are lucky your code will crash. If not debugging can be tricky. Stack imbalances won't be reported/checked for you, there is no bounds checking for anything (not even structs). But there are conventions/strategies to prevent these kinds of bugs from happening that one needs to either discover for themselves, or find out in books (the book Thinking Forth helps, but is not enough I would say).
Working with the stack can be more difficult than with registers, although the latter can be easily simulated with variables, which is often frowned upon. But words like CREATE ... DOES> enables meta-programming that helps with generating code with code, and makes it quite powerful, but can make your code complicated to reason about (see the concepts of compilation vs. execution vs. interpretation semantics described in ANS Forth).
In the end the appeal of FORTH for me is in its "simplicity" (but nowhere any ease of use as it requires mastery of laying out code in memory as you would do in an assembler without any help from the language itself), its overall philosophy (see Chuck Moore's A Problem Oriented Language) on focusing on the nature of the problem rather than thinking in terms of the tools/language given to you (build a "language" for the problem), and providing solutions with as minimal "cruft" as possible.
Anything that you say is missing can be added. I'm no expert but when I had some confusion about the stack I would create a word that did something in a balanced way, test it quickly, and use that word instead to build on. Forth makes it easy to climb the levels abstraction quickly.
The method that it uses to interpret/compile a word varies by implementation. Subroutine call is just one of them.
> Anything that you say is missing can be added.
For sure, it can be extended indefinitely. It's good that you made that clear. You can add a C compiler if you like (see Dusk OS) even, or a generic BNF parser generator (see Bradford Rodriguez) into "the language". Anything that you devise for code correctness, and static analysis can be added. My points about the lack of these language features were towards the previous comment about FORTH looking "more high-level than C". These are definitely major shortcoming for an inexperienced programmer to be able to do anything reasonably complex in FORTH (similar to using assembly).
> ... I would create a word that did something in a balanced way, test it quickly, and use that word instead to build on. Forth makes it easy to climb the levels abstraction quickly.
I would say any programming language with functions provide the same ease by that definition. That is, in each you can write a set of functions, than use them to compose higher level functions ad infinitum until you create a DSL as essentially a collection of functions for your problem space. Although doing so in C-like languages syntactically it may look more like Lisp than FORTH. In FORTH it looks more concise, and reads left-to-right thanks to it being accidentally a "concatenative language" with point-free notation and combinatory calculus roots. A great example of this being formalized is Joy by Manfred von Thun.
So I think what makes FORTH unique is more of the philosophy around it (again, see POL book by Chuck), which is a kind of zealous struggle for simplicity, but not easy, and keeping the problem in focus and not ignoring what's inside the boxes you build upon. You could say it's a panacea for yak-shaving if done right. Concrete examples for what FORTH does away in its search for simplicity and avoidance of yak-shaving, here are a few:
- no in-fix notation or ASTs: computers understand post/reverse-Polish by default by virtue of them being a stack(/register) machine, the programmer can do this work without major difficulty, - no filesystem: blocks and table of block indices are enough most of the time, - no floating point: fixed point is good enough for most problems, - no classes, arrays, structs: you can build your own constructs suited for your problem as they are needed, there is no one size fits all solution,
Etc. The list goes on. Some of these are added into the so-called standards, but some argue trying to standardize FORTH defeats itself.
> The method that it uses to interpret/compile a word varies by implementation. Subroutine call is just one of them.
I used a vague terminology there, and should have clarified by saying "regardless of the threading model". What I meant was that effectively the FORTH source compilation is a one step pass that converts string tokens into series of "subroutine" (conceptually) calls that map 1:1 with the source code (homoiconicity); direct/indirected threaded, token threaded, or subroutine threaded all effectively is laying out/interpreting a series of subroutine calls, including those in FORTH CPUs like Harris RTX or GA144.
> Working with the stack can be more difficult than with registers, although the latter can be easily simulated with variables, which is often frowned upon
Yet every time I hear experienced Forth developers recommending to use more variables, and that newbies tend to eschew them, making the code much harder to read and understand than it is necessary.
You become a true Forth programmer once you go past the code golf and stack juggle phase.
Factor addresses some of these concerns and instead of giving you a bare metal REPL you get a Smalltalk-like image: https://factorcode.org/
It's rather neat.
I was just looking at Factor yesterday. I find it cool that it was originally created as a scripting language for a game engine.
Old 8 bits "family/home computers" were designed with teens and kids in mind - perhaps an influence of Alan Kay? BASIC means "Beginner's All-purpose Symbolic Instruction Code", so it was a good fit. The Amstrad CPC 6128 also came with a Logo implementation [1], which was an educational dialect of Lisp (I still remember how its associative lists blew my mind as a kid), on a separate disc.
Also, at that time "open source" wasn't really a thing, compilers/interpreters for various languages were professional, commercial tools [2].
[1] https://en.wikipedia.org/wiki/Amstrad_CPC#Other_languages [2] https://en.wikipedia.org/wiki/An_Open_Letter_to_Hobbyists
I think writing a toy Forth interpreter is a good way to learn about the language. It's easy and I had a lot of fun with it. But it is so ridiculously easy, at least up to a certain point, some may find it too elementary, or too tempting to go beyond a toy implementation.
Even just reading through an existing implementation can be very enlightening. E.g. JonesForth (where like 90% of the assembly source is comments explaining everything in detail): https://github.com/nornagon/jonesforth/blob/master/jonesfort...
I really liked JonesForth, and I ported it to powerpc about a decade ago (maybe longer, I don't unfortunately have it around anymore to check, nor a machine to run it on). One thing that was very frustrating was finding out that the macro system was much more limited under darwin's assembler—this resulted in a painful translation process and too much M4 usage. If you do go down this route, I highly recommend fully exploring the capabilities of the assembler's macro system before diving in.
Very fast (faster than naive assembler) but not at all high-level; having to look after the stack is a bit of a pain. Writing your own FORTH is fun - it doesn't need to be in assembler - I once wrote a FORTH-like scripting language in C++.
> Very fast (faster than naive assembler)
Every Forth that uses conventional threaded-code interpretation pays a considerable performance penalty, execution times are likely to be very roughly quadruple the equivalent assembly. [0]
Forth's runtime performance can be competitive with C if 'proper' compilation is performed, though. [1]
[0] https://benhoyt.com/writings/count-words/
[1] (.fth file with their results in comments) http://www.mpeforth.com/arena/benchmrk.fth
This is true. It's not terribly difficult to bootstrap an (inlining) compiler from a threaded interpreter, though, including eliding a lot of stack operations.
I'm curious if anyone has tried using futamura projections to do this in stages. I hadn't known about them when I last built a forth and it may yield more satisfying, incremental results than the last time.
but it's also very extendable. It's ability to slap on new control structures and DSL's is on par with Lisp. I'd say it's very low level and much higher level than the most languages simultaneously.
yes, that's what i did on the C++ implementation i mentioned. it was for writing the action parts (not the parser etc.) for a text adventure system.
Yeah, that's what I was kind of getting at. It looks like it starts low level, but it seems like it allows effectively an infinite amount of abstraction.
> Very fast (faster than naive assembler)
Depends on how naive the assembler programmer is, and, I would think rarely, if ever, on modern hardware because the many subroutine calls kill branch prediction benefits. Also, on lots of old 8-bit hardware, defaulting to 16-bit integers will kill performance relative to native assembly in cases where 8-bit integers suffice.
(Of course, you can fairly easily replace hot loops by assembly or (more difficult) change the forth compiler to compile parts to native code, fuse words, etc)
One of the great things about it was, it came with an assembler vocabulary to code your inner loops or other lowest-level stuff in. I gather BBC Basic had something like that, but I never saw and and I did get to use Forth in this way back in the day. Most of those systems made it harder to flexibly mix the higher and lower-level coding.
This is much like C. The easiest way to use assembly in C or Forth is to know your 'ABI' and write separate assembly code functions where needed. In Forth at least you can write a CODE word.
Yes, though:
- high-level Forth also amounts to a Turing-complete macro assembler (much better than textual macros)
- C was less practical/available on early personal computers, especially for coding right on the target system. When I was doing this it was on a TI 99/4A in the early 80s.
Forth is very good for writing small software in tight memory constraints. Unfortunately it is pretty hard to read; for bigger software projects there are many better languages.
I once heard Forth called a "write-only language."
Forth has been something I've wanted to learn for years now.
You can buy SBCs that run FORTH natively. Just plug a USB cable into your computer, fire up a terminal program, and you're ready to go. It's a great way to get completely immersed in the language.
(One word of caution: Mine took several months to arrive from Australia. Look for a supplier close to you first!)
Another great way to learn FORTH is to do it like it was 1984. Load up a Commodore 64, Apple ][ or similar emulator on your modern computer, then load a FORTH language IDE into that.
The documentation of that era was written for people who were completely new to the arena, so it's tremendously easy to follow along with, and available as free scanned PDFs on the internet.
> You can buy SBCs that run FORTH natively
Even very cheap and readily available microcontrollers like the STM8 can be used in this way [0] [1]
I’ve found the microcontroller for less than 20 cents on Ali express, easy to solder. Add a capacitor and a TSSOP-to-DIP adapter board and you have a breakout board for less than 50 cents that can run Forth.
[0] https://github.com/TG9541/stm8ef
[1] https://hackaday.io/project/16097-eforth-for-cheap-stm8s-gad...
ValForth from Valpar was one of the first cross platform FORTH implementations in the Atari ST ecosystem, and it had some clever extensions for games.
https://www.atarimagazines.com/rom/issue1/jumping_forth.php
But like the post mentions, even the 8-bits had FORTH from Elcomp, and books like https://www.atarimania.com/documents/FORTH-on-the-Atari-Lear.... Leo Brodie's "Starting FORTH" is still a great intro.
While we all learned BASIC, these alt languages helped us learn that there actually are radically different metaphors to program the device
Mind expanding to a kid in the 80s!
Looks very cool!
Relatedly, there's http://tumbleforth.hardcoded.net/, which I think looks lovely. Has anyone gone through that and would like to share their experience?
I got about halfway through it during a slow work week. It was a throwback to my hardware classes from college. It got me thinking differently about computing.
I am young and stupid, but from a rear-view perspective it looks like maybe certain abstractions were chosen in the old days when there were hardware limitations, and that our current "evolutionary branch" of programming languages has a lot of abstractions that have not aged well, leading to a lot of layers of load-bearing cruft --much like any engineering project.
Collapse OS might not be practical today, but it has a "liberating" appeal. Freeing yourself from all these layers of abstraction sounds really enticing. A way to enjoy computing as it existed in the 1960s, but without the terrible developer experience. (or so I imagine)
Currently my pie-in-the-sky project would be to work through these projects, get Dusk OS building on a virtual machine, then physical machine, then write a Scheme interpreter for Dusk OS in C --and go hog-wild from there.
I have a couple of rivers to cross before I get there. I implemented a Scheme interpreter in Python in a couple of hours, then improved the scanner/Tokenizer in a couple more hours. Now I'm reading through crafting interpreters to see how I would go about implementing a Scheme interpreter in C. After that's done and I implement an interpreter in C, I'll revisit this guide and try to jump headfirst into DuskOS.
Ok, I'm tempted to go back to it, thanks for sharing your experience! I have had some ideas similar to what you describe. I wonder if you've seen https://wiki.xxiivv.com/site/uxntal.html. Some of the projects listed here might be of interest to you either https://malleable.systems/catalog/
That malleable systems manifesto really resonated with me. I actually did a project recently where I tried to adhere to that sort of ethos: https://pickles976.github.io/Hari-Recipes/
It is nice to see others with similar feelings.
Famous last words :)
I was adding a scripting language to an application, I just needed a scripting language, that was 16 years ago.
I found this quite easy to follow: https://www.buildyourownlisp.com/ for building a not-quite-Scheme in C. I didn't get massively far but only because of the sheer amount of other shiny things.
Yes I have seen that one! It's on my list of resources. There's also this, which I have been studying the code of as I follow along with Crafting Interpreters, to try and incrementally understand the codebase:
https://github.com/vibhavp/skeem/blob/master/src/builtins.c#...
The simplicity of the eval function is so cool!
Just started looking at it, thanks for sharing.
I already like his style of talking.
Examples:
>Much terser than the C version right?
>This little assembler crash course gives us a better understanding of what is compiled by the C compiler, but not how it compiles it. We don't know how it's ran either. To know that, we'd have to dig through software that weighs millions of lines of code. Maybe you'd have the patience to do it, I don't, so let's continue tumbling down the rabbit hole. We'll go bare metal and then build an operating system of our own, with a C compiler of our own. It's simpler this way.
>What’s a linker? Aw, forget about it, it’s another piece of overcomplicated software that has convinced the world that it’s essential. We won’t need one in what’s coming.
:)
Yes, he's great! More character and joking around in technical writing. Donald Knuth, Ketman, Starting Forth, Land of Lisp, probably lots of others I'm forgetting... it can be done, people!
The Crafting Interpreters book author is another one, at least mildly. I've not read the book fully yet, but have skimmed the first chapter or so.
Yes, absolutely, agreed! I read a couple chapters too. And another one I forgot earlier is Janet for Mortals.
Virgil Dupras, the author of the Tumble Forth series there, also authors DuskOS, an OS written entirely in a custom Forth. His consistent and prodigious output really is quite impressive. I don't really hold to the collapse philosophy, but the DuskOS mailing list has just the right amount of volume that it's perfect for lurking.
Virgil's work inspired me to give Forth a bit of a go, and last year I spent some time hand decompiling SmithForth[1]. It really is remarkable how little assembler is required to bootstrap the language. I can totally see how Forth could give you a repl in embedded environments, which sounds way more fun than the typical dev cycle I hear about.
Just following up on this - this comment was my initial excitement at seeing some real Forth-based thing. I've had time since then to follow up and read the wikipedia page for ChipWits now and the blog post: the original game looks very cool. The logo from the manual on WP is so great.
I'd been making a list recently of games (old and new) that have puzzles and programming type stuff in them. It's going on the list! I could very well get on to it next after I'm finished the ketman assembly "game" (although it's not really a game, I suppose).
I hope the new ChipWits does excellently!
That's so cool - where can we find the list?
for your list, a few more entries, if not there already: https://news.ycombinator.com/item?id=7118649
and you should post your list here.
Added! Thank you, how lovely, that is indeed very good. You've prompted me to get on with that project, and I'm organising, compiling, and getting that list ready to publish here now.
Thanks a bunch. We have high hopes for our game.
I so desperately wanted this game as a kid.
Man I'm getting old.
I bought and enjoyed most of Human Resource Machine (until it got too difficult)
It’s seems it is based on the same concept as chipwits (stack based programming as a game)
Anyone played both? How do they compare?
I played it a lot on the Commodore 64 when I was a kid. Unique, curious and entertaining.
We kept the gameplay of the original pretty much intact in the new game. I don't want to spam Hacker News, but since you played the original, I'd love to get your reaction to the reboot: https://chipwits.com
Forth seems to be one of those write once languages like perl. Easy to start writing and building, but come back to the code in a year or so, no clue what it does.
But really fast and efficient.
Anecdotally I find the opposite to be the case. I think Forth is hard to write but easy to read.
Makes sense, because to write it you have to understand everything, which means keeping code super well organized.
Curious, what is the modern version of Chipwits is written in?
They mention that it's C# and that it's at least 10x the amount of code...
I loved this game! I actually had the Forth package for the C64 also but I never put 2+2 together that the game was made with it.
Glad you dig my old game. Play our demo on Steam!
That's awesome. I will! I was probably around 12-14? when I spent many hours with that game. After that it was DOS and Doom. But I think when I eventually drifted towards the path to being a software engineer I did grasp a lot of the basic concepts faster because I played games like this as a kid. The other path was being a musician of course.
Stacks in ChipWits were very much a result of coding the game in FORTH.
Unity in C#
Crafted by Rajat
Source Code