I made a sorta-proof-of-concept game for the original Game Boy in Nim (with the help of GBDK/SDCC as the compiler), with custom assembly bootstrapping and minimal dependency on GBDK libraries.
https://github.com/ZoomTen/pocket-clicker
Basically, it's a very simple (and very boring) Cookie Clicker clone; nothing much at the moment.
I've been using Nim for a few months now and I've really enjoyed it. Personally, Nim's highlight for me is how I can make applications and scripts that are performant yet easy to read and write.
It advertises itself mainly as a "systems programming language" (though IMO it doesn't necessarily need to be), and that much is proven with projects to program Arduino, ESP32 devices, and even the existence of @exelotl's Natu toolkit for creating Game Boy Advance games along with successful projects made with it.
The latter of which made me think if it was even possible to do with the original Game Boy—that would be really funny, I thought. The mere existence of a C compiler for Game Boy proved to be enough to try.
This is actually my second attempt at doing such a thing—my first used a lot of bindings to GBDK and remained careful not to use things that require managed memory like Nim strings and seqs: https://github.com/ZoomTen/nim-gb-test
I started out this project trying to compile some standard Nim (that is, your basic Hello World) with SDCC to see what kind of stuff I needed to work around, using things like -d:danger, -d:useMalloc, and --checks:off. In lieu of the Makefile I previously used, I decided to use Nim's tasks instead.
Seeing how os:any seems to be recommended over os:standalone, I tried adding that. Unfortunately, I quickly realized that there's still a major difference between os:any and os:standalone at the moment, namely the former assumes in its system module that a standardized output device even exists for the architecture (FILE*, stderr, stdout). I could spoof the incompatible calls out in my custom nimbase.h, but C macro substitution could only do so much, and I eventually hit a roadblock (specifically, I couldn't patch out nimDestroyAndDispose yet). So I switched back to the tried and tested os:standalone.
With a few calls to fwrite and such spoofed out to 0, it compiled. Overriding echo with a call to GBDK's printf (and of course converting the Nim string to a cstring) made it display the text. But what was more surprising though, was the fact that even seqs, objects, and repr-ing them worked too.
That discovery motivated me to go further with the idea. Here I faced another problem of not being able to specify the C compiler invocation template. To get around it, I had to use NimScript. Since I couldn't exactly use the .exe and .options.always parameters to use the current Nim binary with the correct arguments, the NimScript needed to be executed directly as if it was a standard shell script (which is the point, isn't it?)
There were some more problems I encountered that I eventually worked around, such as SDCC crashing with "unbalanced stack" errors with certain parts of Nim's C codegen, and the .uint8 / 'u8 litter needed for performant ASM codegen and type satisfaction.
The end result of all that, however, is a program written (mostly) in Nim, its compilation controlled with Nim, and I gained a level of control over the output slightly higher than what stock GBDK would grant me (thanks to being able to compile my ASM code in the same build process).
Ultimately I think this endeavor proved to me even more that Nim really can be described as a "systems language", even if some elbow grease is needed to make it work on exotic architectures. Standing on the shoulders of giants turned out to be a very good idea :)
This is really cool! Fascinating to see how relatively easy it is to use Nim for these older system.
Curious to see how you overrode echo. I know of a couple ways it can be achieved, but I'm interested to see if you found another way.
My earlier try was as simple as a proc echo (s: string) = printf((s & "\n\n").cstring) in the same file :P (yeah, no varargs…) I am aware of the term-rewriting macro method however, but this turned out not to be necessary yet in my case.
Though I have since made my version also take a VRAM pointer argument, so I can place text anywhere I want.
I may have mentioned it before, but bitsets are an amazing feature to use, the resulting code is more intuitive and readable with things like:
if (buttonB in joyState):
enableLcdcFeatures({bgEnable, objEnable})
while busy in rStat[]: discard # wait until VRAM is safe to access
And I also remarked in the code about compile-time table generation, which is more convenient than defining weird macros in ASM, and also works very well with graphing calculators for prototyping the needed formulas :)