Celes' Ramblings

The Language That Never Was

Now, where do I even start... :bunhdthink: Ah, games, yes I guess that's a good start. Some people like to play games, videogames even. But videogames don't grow on trees, you know?

Look, mind's still fuzzy. I don't usually write in words :akko_nope: I could go all the way back to John Carmack being an alleged asshole to the other John sitting in a garage making Doom or something, don't ask me. Then something something fast forward and there's Jonathan Blow influencing the next generation of wannabe C replacements while he sits comfortably in his private-boat-club driven development scheme working on that sokoban game. And then there's an influentiable 10-year-ago me, watching those streams and thinking: Surely what the world needs is another programming language for making videogames. Except... I thought so unironically, and I still kinda do?

Yes. That would sum up the context nicely. Let's get started. :akko_fistup:

Chapter 1

What Do I Need In A Gamedev Language

Language design is a very opinionated thing, and game development is also a very opinionated thing. Everyone has different answers here, but I'd like to think my requirements lie somewhere on a reasonable middle-ground and represent at least another person.

It is not unheard of that games need to run fast, despite the fact nobody can agree on what that really means. Thankfully, as the old saying goes, computers are fast. But as the other saying goes, picking Python the wrong tool for the job will make computers go slow. It's not a surprise that most discussions around language design for game development focus around performance. So to ease ourselves in on this journey, let's start there.

Gotta Go Fast :sanic:

As far as I'm concerned, there is one key property in languages that separates languages that can do this (CAREFUL: turn the volume down!) from those that don't. And it's not discussed often enough. It's value types.

By value types, I mean, the language must have a notion of struct (object, potato, thing, record, whatever) where the language guarantees that:

Most languages offer a limited form of this. Except for pathologically dynamic cases (not gonna take another stab at the snake, we all know who we're talking about), an int's an int, ints aren't boxed and thus follow most of these properties. But the point is to allow custom, user-defined value types. That's what allows us to climb up the ladder of abstraction without losing performance. Primitives being value types is not enough.

And note how I didn't mention a property of value types that some consider essential: immutability. Actually, if anything, I'd prefer my value types to be very mutable. As mutable as I can get away with. Games are big mutation machines. In that sense, I lost my faith in Project Valhalla the moment I realized after all these years they were only going for immutable value types because they couldn't figure out how to make mutability work. That day was a sad day... But let's not dawdle!

Lots of young fursuit wearers (Hey! I'm wearing cat ears as I type this, I'm with y'all :blobcatheart:) are spending their bandwidth thonking intensely about some flavor of automatic memory management, more often than not involving checks and borrows. To me, that's an interesting and valid question to tackle but, ultimately, the wrong angle for a game development language. I'm more interested in having the properties I discussed above than anything to do with ownership, RAII or lifetimes. The way I see it, it's not that hard to take whatever system your language offers and make it perform well: Be it garbage collector, reference counting, generational references (hi Vale author! huge fan!!11 I love the side notes thing in your blog), or the infamous borrow checker :ferris:. If you want to be fast, you'll preallocate, reuse memory and avoid allocations in your hot loop. And that's just as true for C and Rust as it is for any flavor of GC'd language.

And speaking of things people often don't care enough about. There's another thing I wish language designers spent more thonking cycles on, one that is also essential to game development: It's metaprogramming.

I Never Finished Let Over Lambda :pensivecat:

Metaprogramming, what an interesting concept. Like the ancient saying goes: "Yo dawg, heard you like programming...". But silly remarks aside, it is one of the most important concepts for a game development language, and to be honest, for a language in general.

Games are huge beasts. They're a long way from the "do one thing and do it well" style of programs other people may be used to. Games are user interfaces, world simulations, pieces of art and gambling machine software all at once. Games do a lot of things, most of them poorly. This is why the average game codebase is scattered with all sorts of inter-related systems that all need to run: Sometimes caring about each other, sometimes not. Games are a visual craft, and you cannot do everything in code. More often than not you need to look at things, tweak things... You need an editor. Little gizmos everywhere. Gizmos is where it's at tbh. :ablobcatnodfast:

But I digress. What I mean to say by all this is that there are two paths to achieve all those things I'm mentioning: Power through it via large amounts of boilerplate or embrace metaprogramming. That is, automating that boilerplate, so we don't have to type it. I've seen people be successful in boilerplate-embracing scenarios, good for them! But I choose the latter out of necessity.

Metaprogramming can mean many things, however, so let me take a moment to clarify what kinds of metaprogramming I'm interested in:

Compilers shouldn't be black boxes, the closer a compiler gets to a library, the happier I am. This is why most flavors of Lisp tickle my brain the right way. I just wish Lisps had a strong flavor of a non-copium static type system (no, I don't mean CLOS, go away *sprays you with water*). And, as chance would have it, that takes us to the next section.

Meet Me At The Top Of The Ivory Tower :utena:

If there's one topic that causes more collective thonk CPU cycles to be spent in language design than memory management, that can only be type systems. I'll start the section by misquoting several smart people by saying that a good type system is like dancing with the compiler. No words, only feelings. The compiler's warm embrace :akko_blush: and also the compiler occasionally stepping on your toe and vomiting a three-thousand-line trait solver error on top of your dress. :akko_weary:

In general I like types, and I like typing them out. As I often put it, if your compiler can't even tell its strings from its ints I have no business with it. I don't care about your "well, it's technically strongly dynamically typed" guido-pedantry. What I care about is about the language saving me from my own stupidity. But at the same time, I acknowledge that can take many shapes besides Haskell's: Because more often than not, it's less about the types, and more about the tooling.

Now, it pains me to say this as much as anyone else, but Micro$oft proved that you can take Javascript and Python and turn them into something I don't mind touching as long as the tooling around them is good. And yes, I hear you, why cast the shadow of the corporate monster over this fun and so-far lighthearted post? Well believe it or not I'm trying to make a bittersweet point here, just need a few thousand more words. Hang on. But for now let's get back on track by saying, that many people don't care about the compiler, they care about the tooling.

Now, if you're one of those people who spend way too much time looking at the terminal output of gcc you may be surprised by this, but most people have the compiler integrated into their editor in a tight feedback loop: They type in code, and red squiggles appear. It's magical. :sparkletrans: I like the red squiggles, I like them a lot. In the old days, we got the squiggles by programming in Java, using Eclipse. :duke: But these days, we just use this thing called the Language Server Protocol. Wait, what's that again? I got this eerie feeling. A chill down my spine. This is getting ominous... But ah, it's gone. Must've been the wind, let's continue.

My point, albeit long-winded, is that your type system is important, very much so, but the way you present that type system to your users is even more important. You need good error messages, you need a good (and responsive, and fast) LSP. And sometimes that means making compromises: Let's take a stab at scala now, and hereby declare that no language shall ever vomit the equivalent of "I couldn't solve this restriction system in time. Sorry!" upon their users. Never. If your type system is so complicated that it prevents your tooling for offering a pleasant user experience, your type system is too complicated.

While I don't mind the occasional trip to the ivory tower myself—believe me I know my way around type fuckery—as far as game development is concerned I believe our requirements are actually quite simple. In general, a language for gamedev should spend its innovation points elsewhere and leave dependent types for whoever needs them. I could make a list of things I want or don't want in a type system, but to be honest I don't feel qualified. After spending years chained up at Rich Hickey's dungeon inside the ivory tower (if you got any weird ideas, that's on you), I have learned to embrace whatever is there and try to make it work. But if I were to write such a list, it would most likely involve things like letting me define my own generic types and structures, as long as most sensible uses of higher order functions and lambdas.

I'd also have a neutral list for things like type inference: I enjoy it, but I also don't need that much of it. Hindley-Milner is cool but sometimes things get Scala-y and the user experience suffers. Sometimes a crappy auto can get the job done just as well. I feel languages are right in converging towards the idea that function parameters should be typed out but inference for local variables inside of functions is fine.

And then there would be the big no-nos list with things like the typestate pattern, anything people refer to as "encoding invariants into the type system" (if it encodes an invariant but you're just casual about it, then it's all good) and especially any linear algebra API that tries to abstract over the storage and number of dimensions via use of templates / generics. But I acknowledge these no-nos are more of a cultural issue. I wouldn't disable these options at the language level. The fact those things are possible indicates a robust and bug-free experience for the normal uses of the type system, so they stay. Instead, I'd disable them at the developer level by using the power of the cult of personality invested in me as a language designer, instructing my minions to point and shame those who use them.

Back to Lisp, for some esoteric reason. One reason lispers give up all their clothes and all their types when enrolling into the cult is because types hurt the "REPL" experience. After all, if code is too static, how could we ever hook into the production server and fix bugs live over the phone, right? That's where we're going next.

Hot And Bothered Reloading

I spend a lot of time thinking about that 57-page PhD-level dissertation of a blog post on why the :ferris: is not a good language for game development, by Loglog Games. Having read a lot of the comments around that post, one of the main points people took home was that the thing that matters the most for game development is iteration time. And rightly so, to be honest! :ablobcatnodfast: I'm glad people got the message, I wish my message was as clear. To me, iteration speed is the single most important property a game development programming language should optimize for.

Now I'll preface all this by being brutally honest again and mention that if your only idea of "fast iteration" time involves a very fast compiler that can compile 100kloc codebase in under a few seconds so that you can close your game, and launch it again from the main menu every time you make a tiny change, you've already lost me. Because It is not about compilation speed, it is about keeping the flow going.

Don't get me wrong, compile times are important. But more often than not, what we need is something that keeps us in that flow state. Able to iterate and get the perfect game feel. Focus on that feature that will make your brain go brrr on the happy brain juice... And, well, for that you need something that will let us iterate on the code while the code is running, without having to close the game and launch it again.

It pains me to see how many developers have grown used to and are perfectly happy with a brutally sisyphean development cycle that involves:

  1. Starting the game.
  2. Navigating through menus to get into the gameplay state.
  3. Test the feature they're currently working on.
  4. Notice something's wrong. Close game, tweak code a bit.
  5. Go back to 1.

If I have something to thank Mr. Hickey for after all these years in the dungeon, it's having been exposed to the idea of a REPL. I do not conceive programming without something at least as half as fun as that anymore. Especially not when we're talking about a discipline as reliant on iteration speed as the development of silly videogames. If you haven't yet, go watch Tomorrow Corporation's Tech Demo and come back after your mind has been blown. Once you're done, let's tone things down a notch so that us mortals can still comprehend them, and let's enter the realm of hot reloading.

It's not that hard to understand really, you've probably been exposed to this one way or another, but seeing how many people are stuck pushing that boulder uphill, let me be didactical for once. :akko_nope: This is what I expect from my ideal gamedev language in terms of hot reloading:

So far we haven't asked for that much, and yet, hot reloading really throws a wrench at the whole thing. So far, my requirements of value types and type system were reasonably fulfilled by most C-replacement-wannabes out there, metaprogramming not so much. But you introduce hot reloading into the wishlist and then suddenly everything falls apart.

Most of the interpreted, JIT-ed or otherwise runtime-powered languages out there (including the JVM that makes our little Duke friend here dance :duke:) have no real trouble with upholding the reloading requirements above. The problem comes when we try to get some notion of low-levelness, you know, like value types. (Those chills again...? let's proceed and ignore this ominous feeling).

But fear not, for in the land of the low level languages a younger and softer-skinned 10-year-ago version of Jon Blow's wholesome alter-ego, Casey Muratori, :blobcatheart: proved all this is possible, McGyver style. As long as you have a dynamic library, a pair of system calls, and a tiny bit of patience. Truly remarkable, and it really gets the job done.

So, problem solved? What's stopping all those C-wannabes from doing the same thing? Well, glad you asked. Because at the end of the day, your favorite C replacement may quack like a C 🦆, it may even look a bit like a C 🪿, but then you blink and you realize you're stuck with the fucking C ABI. And with that, let's smoothly slide onto to the next section so I can make a simulacrum of a point. :akko_nope:

The Interface Of The Binary Application, And Why I'm Forced To Care :neobot:

By this point you may have realized I've been typing words about lots of things I only have a vague understanding of. Let me tell you it's mostly the world's fault. I just wanted to make your screen go beep boop in fun little colors, I did not choose the life where I have to care about what an ABI is, that life chose me...

Let's misdefine the ABI, as far as a game developer like me is concerned, by saying it is how the computer arranges the bits of your things. Especially when those things need to be transported around various parts of your code, like when you pass a parameter to a function. The ABI is the set of rules that tell the compiler—or anyone who enjoys typing assembly by hand—how to do that. It's important to follow the ABI so that the receiving end knows how to unpack and interpret all these bits you arranged for it before jumping to the first instruction of another function. That is, if someone didn't manage to overflow your stack, do some nasty things, and suddenly you're jumping into the first instruction of a fun ransomware adventure.

The reality of software these days is that we're stuck with the C ABI. Lots has been written about this. Some are trying to change it, others are... well, promising they will change it. At the end of the day, while I still have respect for all these attempts, the reality of the situation is not changing anytime soon. And that reality is that the only way two different computer languages can talk to each other is if they talk in C. Or rather, the C ABI.

And let me point out "C ABI" is a bit of a misnomer, because there's not just one C ABI, there's one for every combination of cpu architecture, operating system, and then some. But what all of them have in common, is that they dictate how to arrange all the constructs of the C programming language into bits, and nothing else. Does C have structs? Yes. The C ABI will tell you what to do with them. Does C have strings? No. Well then the C ABI says you're on your own.

So, why do we care? I could tell you all the benefits of having good cross-language interop, which I actually care about, but that's not the main reason we're discussing ABIs. After all, we're happy with a single language as long as that language is good. And for those cases where we need to call out to the outside world, just use the C ABI and write some glue code once. Who cares, right? Well, this takes us back to the fond nostalgic memories of the previous section: It's hot reloading, all this is still about hot reloading.

Because, see, the problem with what I'll from now on refer to as Casey's DLL trick, is that it works for any language, but if your language isn't C, then you're forced to dumb-down to C at the point where your hot reloadable code and the non-reloadable code meet: The dynamic library barrier.

If you didn't watch the six hundred episodes of Handmade Hero, let's take a moment to explain what Casey's DLL trick to hot reloading game code is. What you do is have a main binary, holding your engine code, and your game code compiled as a dynamic library. The game code calls into the engine, and the engine is watching the source files for the game code. When the game code files are updated, the engine recompiles the dynamic library, unloads leaks the old one, and then loads in a fresh new dynamic library with the new code. The next time it calls into your game code, the new code runs, magic! :sparkletrans:

For Casey the trick works perfectly because he's old school. He writes in C. And maybe there's a lesson to be learned there, but my mind is too contaminated by all those years in the ivory tower to be touching C anymore. If I can be honest for a second, sometimes I wish I could just write C, safety concerns aside. :pensivecat:

When your language is not C, any communication across the dynamic library barrier has to happen in C. That means if your objects have vtable pointers... well, good luck with that! And what if your language allows higher order functions that hold state (aka lambdas, closures...)? No it doesn't! Good luck rebuilding civilization from scratch because in C land you only have a stone, a stick, and a pointer. And you're on your fucking own.

Unless... you realize there was a bit of a lie here, a sleight-of-hand if you will, because there's an alternative: Your language may not give a damn about that C ABI as long as it offers its own stable ABI. A language having a stable ABI means that the language can talk to itself across different binaries (in Casey's DLL trick, the dynamic library with game code, and the game engine) using all the constructs that are representable by the language!

So, let's take a quick moment to appreciate all the languages that sound like good candidates for gamedev on paper and have a stable ABI:

Oh shoot, it's only C, is it? :neofox_laugh_sweat:

Ah, and Swift I've been told. Thanks wonderful fedi people. :blobcatheart: Apparently Swift has a stable ABI too, but I don't own an Apple device so I can't check for myself.

Still, If we lower our expectations from: "two independently compiled binaries, created by two separate versions of the compiler running on different machines at different points in time must talk to each other", to the much softer requirement that: "two binaries compiled by the same compiler, on the same machine, on the same day, without rebooting the computer in-between must talk to each other", then some of the languages on that list above can somewhat hold their ground, but the issues is that there are no guarantees.

The main issue here is, without the explicit support and blessing from compiler developers, any trick that relies on reloading dynamic libraries for a compiler that doesn't offer that stable ABI could break at any point. This is not a made-up fear: For many languages, things are working fine in your little game jam demo but break in catastrophic ways once it's too late and your codebase has grown. And when you report it as a bug, you'll be very politely told by the GitHub™ stalebot to fuck off, because nobody will care about your little hack.

In my experience, compiler developers tend to get mad when you remind them their ABI is not stable. Much like my mind, it's unstable for a reason, you know. In their case, it's so they don't have to commit to any decisions because most people struggle with commitment the language can keep evolving without being restrained by past choices. A commendable goal, which has nothing to do with my requirements by the way. Evolve all you want, I just want you to tell me I can compile two binaries with the same compiler version and have them kiss talk to each other. I haven't seen enough people ask for this, I want this. Discourse is polarized between the "stable ABI all the way" vs "let's randomize struct layout on every compiler invocation for security reasons", with no nuance in between.

And it's convenient that I've run out of clever ways to tie the sections together with one another, because the remaining section is a bit of a mixed bag of features I would like to see in a language but did not qualify for a full-blown section.

... And Then Some

There's plenty of things in a programming language, so here's a list of more things I've come to appreciate in all my years of computer touching and I value in the context of videogame programming:

Whew! And that concludes our first chapter of the blog post. After all this opiniondumping, if you haven't left to rage in the comments yet, I hope we're at least somewhat aligned into what a game development language needs. :ablobcatnodfast:

Now it's time to get a bit more personal. Because, you see, there was a time where I tried to take matters into my own hands. I tried to do something about all these ideas. This blog post is actually the story about...

Chapter 2

That Time I Set Out To Make A Programming Language

There's a time in every developer's life when they're exposed to the beautiful world of compiler development, often via the gateway drug of parsing. Some of them recover from their parsing-induced trips and go on to discover the world of semantics... and this is how new babies are made programming languages are born.

I was one of those developers. Young, influentiable, with a pure and bright gaze... And without repeating myself too much, having the pleasure of being there for that first Jai talk at the same time I was taking my first class on compilers probably ruined my brain in more ways than I care to admit. Compilers have interested me since forever, and my game development obsession began slightly earlier than that. As I kept growing as a developer, my ideas about languages became more refined and ever so slightly less stupid.

Annoyed by the mainstream, just like Shadow the Hedgehog, I grabbed my motorbike and a comically unfitting weapon and sought to discover the edgiest corners of programming. I started doing Haskell, then quickly moved to Clojure and spent more time than I'd care to admit idolatrizing Rich Hickey's ideas. Some people never recover from Lisp, I was lucky I made it out. Then, my gamedev obsession and yearning for static analysis brought me to the language everybody was hyped about: Rust.

Another Crab Bites The Rust :ferris:

The game development community in Rust has been described by some with SEO-juice-fueled newspeak-approved words like vibrant and thriving. Sometimes, when people are pointing out undeniable flaws, the words adapt and morph into others like incipient, or still in its infancy. The success of Rust is a success in storytelling first, language design second: We are the the diverse, the robust, the strong. We are responsible. Our fight is against them. The others.

And to some extent I actually buy all the rhetoric there. Look, if you can't write three lines of code without invoking UB, there's something seriously wrong and most likely unfixable with your language. No amount of public whining is going to change that, and no amount of hand-waving while screaming "safety profiles" can get you out of this mess. I appreciate the good work that went into Rust, and it was a necessity. It's also hard to feel more welcome and included than when you approach their community as an enthusiastic and open-minded newbie. Just don't fly too close to the leadership and your wings won't burn. You'll be happy.

There's plenty written on the topic already. But after spending almost five years trying to make game development in Rust work for me, I feel qualified to at least elaborate on the issues that ultimately brought me to parting ways with it. Even when just having mentioned that, I feel the need to reiterate that my points are about game development. I try to only talk about the things I'm qualified to talk about (and then also ABIs).

But all these reasons pale in comparison to the star of the show. The one guiding principle, ingrained deep within the core premise of Rust, that you shall under no circumstance hold two references to the same data (aliasing) while one of those references is modified (mutability). It's either one or the other. It's an XOR. Aliasing xor mutability.

Aliasing Xor Mutable Cuts Your Wings

There is a bit of a journey every young :ferris: goes through. First, you try to mutate something while holding another reference to it, then you get scolded by the borrow checker. Rinse and repeat, and your mind is slowly shaped by the idea. You start developing coping mechanisms to navigate the restriction and you get better at it. But sometimes you need more, so you find out about interior mutability. You learn to tame it, you internalize it, and get more and more comfortable with controlled bending of the rules. Then you feel invincible, you're a Rustacean, a soldier of the crab army, through and through. You are finally a grown-up crab, and so you go on to spread this useful knowledge. But then, for some of us, one day we realize that all these hoops we're jumping through... there was a time when we didn't use to do any of that? And we start looking at all those naysayers telling you some patterns, like you know the Observer pattern, are not possible in Rust. Suddenly, a switch flips and we see them in a different light. Maybe there's some truth to that? You know you can make anything in Rust, even a linked list! But... at what price? A brief moment of hesitation, the cognitive dissonance becomes too much, things start crumbling, and it's all over. At least that was it for me.

I do not know if I'd be the same programmer today without having gone through this journey myself though. Being subjected to the borrow checker for extended periods of time is truly a mind-altering experience. I am now more conscious about the ownership and borrowing state of my variables, and for the most part I instinctively avoid mutating things while others are looking at them. I'm conscious of when I break that rule. All of that sticks with you. But... now that I don't actually need to worry about those things anymore, let me tell you it feels absolutely liberating in the best possible way :blobcatmelt:

What I internalized (not realized, but internalized) here was that aliasing xor mutability, the core restriction the borrow checker seeks to uphold, is not something you can disable. People had told me before, but I had not internalized it. Deep down, I subconsciously thought I could turn it off. What's interior mutability, if not that? But no, no amount of interior mutability or unsafe will ever let you disable it. If you manage to do it, congratulations: You have now successfully invoked UB, that's the deal with Rust.

Well, I lie a bit. In fact, there is one way to disable aliasing xor mutability: Stick strictly to raw pointers. But at that point you might as well be programming in C. Nobody likes to dereference fields via a combination of ptr::addr_of_mut! and ptr::read when they could type a single ., and the moment you cast that pointer into a reference, you better be well-behaved or else it blows up in your face. By making the only escape hatch, pointers, unyieldingly unergonomic Rust makes sure that nobody ever escapes the one sacred rule and comes out unscathed.

And you would say: Okay, but, isn't that a good thing? Well, for the most part, yes. But, let me tell you a little secret: In C, you can mutate a variable while some other function up in the call stack holds a "reference" to it. Imagine if all those duplicate &self and &mut self methods were no longer necessary. Imagine a world where nobody cares about partial borrows. And no kittens are harmed in the process! Most of the time anyway. If those blasphemous ideas are to be explored, there are two very different scenarios that are worth discussing here:

In the land of the many threads, very little discussion to be made. When there's multiple threads, if one thread something something mutates a variable while another thread reads it, that's called a data race. You don't want those, they're a big source of all sorts of bugs because you can never tell in which order things may happen. In a multithreaded scenario, enforcing aliasing xor mutability is a necessity and I don't think there's anything else that needs to be said on the topic. Rust does very well in that regard.

But in the land of the single thread, aliasing xor mutability is a conscious design decision. A tradeoff, and one that could've been avoided if Rust had taken a different set of forks along the road. Now, before you scream "something something iterator invalidation" at me, please assume some level of competence. I know what I'm talking about. We've got this far after all, and I thought we had something going. Don't ruin this.

There are indeed situations in which things can go wrong, even in a single threaded scenario. A commonly cited example is modifying a collection while you are iterating it, since the allocated buffer holding the data for the collection may reallocate or otherwise morph and the memory you were iterating is suddenly freed. This is something our favorite tiny friend, the little Duke, :duke: solved in the mid 90s by introducing this advanced technique called a dead-simple runtime check (DSRC). In Java, collections check whether they are being iterated before any mutation methods are called, and the collection throws a runtime exception when that happens. In my experience, the situation is rare enough that you'll see very few people claiming they need to catch these mistakes at compile-time. And in modern CPUs (aka anything released in the 21st century), the branch predictor ensures these runtime checks are as free as the array bounds checks you like to boast about when you're dick-measuring against the Cniles, so no you don't get to play the performance card here. :akko_smile2:

Having made a language that took this alternate set of tradeoffs (but more on that next :ablobcatnodfast:), I actually spent an awful lot of time thinking about this, and became aware of the other situations in which breaking aliasing xor mutability makes things go south beyond your favorite reddit counter-argument. In general, the problem occurs whenever you are allowed to hold a reference to something where the proverbial rug can be pulled from under you. A very good example is what happens with rust enums and pattern matching, say we allowed breaking the one sacred rule, then you could write:

match (foo) {
  Bar(ref x) => {
    foo = Baz;
    println!("{x}"); // oh no!
  }
  Baz => {
     // ...
  }
}

There's plenty of situations you need to watch out for, and I'm not saying that's where Rust should go. Rust is fine where it is. What I'm saying, is that I believe a world exists where you could take a different set of forks along the road, and end up with a language that doesn't feel so restrictive. A language that lets us have fun while keeping most of the performance benefits from Rust. At least, when it comes to my tiny bubble of opinionated pragmatic game development.

Just like in every breakup, it's important to be reassuring to the crabs. Give them a warm hug and tell them that it's actually not them, it's me. :blobcatheart: Figuring out Rust wasn't for me took a while. Don't take it too literally though: Many of my criticisms above are shit that you could get together. People are not joking when they say the "right tool for the job" thing. I think for most unyieldingly pragmatic game developers out there, Rust simply isn't the right tool for the job. Rust is about the ideals above all else. Those ideals are (i) safety at any cost, (ii) performance except when it would conflict with safety and then if there's room for it (iii) maybe ergonomics. My priorities are shifted. Safety is up there at the top, and I'm not saying let's all use Python, but I value ergonomics over performance. I only want performance when it doesn't shatter ergonomics. That is a fundamentally irreconcilable difference between me and Rust, so I left.

I've always been a bit of a rebellious spirit. Rust is all about what you can't do, and I just wanna carefreely do stuff. I'm with them, safety is a must, I just don't appreciate all the unjustified dogmatism that comes with it. And if Rust is known for something, that is for being a grumpy parent, yelling at their kid because their room is a mess, they're being annoying with the guitar and they're listening to this "twisted sister" noise... so after years of bottling up frustration I decided to embrace my cringe angsty teenager spirit and declare that we're not gonna take it anymore! :akko_fistup:

Punk's Not Dead

If this were a movie, this is the part of the movie where a music montage starts with scenes of me frantically typing code. The code of a compiler. All those tests slowly going red-to-green. And a game, a game suddenly starting to emerge, written in the language. Those were good times. Imagine thinking you can change the world, make history. The megalomaniac tendencies in me were kicking in hard, I tell you. I'm getting nostalgia just thinking about it. :blobcatmelt:

I set out to build a programming language of my own. One with a set of guiding principles that amounted to: (i) I want to make games, (ii) have fun, and (iii) :ferris: can't tell me what to do. What could go wrong?

Despite the roast in the previous section, I actually liked many things in Rust, still do. It ticks many of the small boxes, although not as many as the big ones. The goal here was to not fully abandon an ecosystem I still held dear. To contribute back, and give the community what I considered at the time would be the solution. Rust's perfect companion. We're talking about a language that:

And I saved the most important bit for last, because it deserves a bit more talking.

Hot Reloading As A First Class Citizen

You heard it, and I hope you're not surprised. The main goal of the language is to have Rust, except with hot reloading. Lots of design space overlap with Mun in that regard. Except my language has strings and arrays and lacks a marketing team and a production grade GitHub™ Actions™ CI pipeline (sorry for the stab, but also: come on! how can you host your first game jam before the language has arrays?).

The way this is achieved is by leveraging the nice, but still very unknown LLVM JIT APIs. It's almost as if by me having shared this, you too are now sitting in this proverbial pile of gold: Turns out that the folks behind the Low Level Virtual Machine that isn't a virtual machine are bad at naming things. Their "JIT" thing is a bit of an accidental misnomer. When people hear JIT they more often than not think of interpreter plus heavily dynamic language plus JIT. That's not what this is about. What you get is LLVM IR compilation at runtime, and your code will run as fast as the equivalent clang -O3'd C if you let it. I measured. And what's the catch? None that I'm aware of. This is the same strategy jank is following. Which by the way look at them! We started pretty much at the same time and I got this silly blog post to show up for it while they are funded! I'm rooting for jank, jank's cool. :blobcatheart:

The big idea is that the full game is compiled as a single LLVM IR module, the same thing Rust does with crates. That takes a while to compile (same as rust crates). But then, on subsequent reloads, only the new functions are compiled, and some monkey-patching function pointer fuckery machinery makes sure the new versions are called. When not inlined, the machinery has a cost, but the cost disappears on release builds. In fact, the whole JIT compilation model disappears on release builds, because this whole thing is built to support AoT compilation, and it actually did, I even had a working proof of concept for it.

It was fun, it was glorious, and I'm not exaggerating when I say most of the things I'm talking about here were actually implemented in that working proof of concept of a compiler. Except for generics, I never reached the point where I needed actual generics, so I had to settle with gonerics. You know, where only a few blessed types in the standard library (including Vec<T>, Ref<T> and Option<T>) were baked into the compiler.

And I mentioned a game! This became the language I used to build the first few versions of my latest game, Carrot Survivors! Here's a gif showcasing the game while I was using this language:

A short looping clip
of a videogame. The aesthetic is pixel art, and quite simple at that. The image looks a
bit burnt, as if someone applied copious, unnecessarily large, amounts of bloom. There is
a character moving around in an empty grass field, they look like a girl, pink pony tail
and wearing some sort of worker clothes and a yellow construction helmet. They're running
around the field, while zombie-looking carrots roam. The character is fighting them.
Several weapons are automatically firing around the character, including some ricocheting
circular saws, some kick particles coming out of their feet, and traffic cones that orbit
around them. After killing a few enemies, the experience bar at the bottom of the screen
fills up and an upgrade selection screen pops up. A user interface shows three different
upgrades: One for the Traffic cone, another for the Coffee and the third one for the
Stapler. The whole thing looks a bit rough and unpolished, but nonetheless appealing. You
may recognize this gameplay loop from games like Vampire Survivors.

Even with all the friction of having to develop a compiler at the same time as the game, the hot reloading made the development experience delightful. This, I believe, should be a lesson for any aspiring compiler developer out there: Have a use case, and dogfood towards it. I can't stress how valuable this experience was. If your language is fueled by toy examples, you will make a toy language. If your language is fueled by the desire of self-hosting itself, you will make a language for making compilers. You have to be your first and most avid user, or else why even do this at all? And, needless to say, if your aspiration is to make the next big language for game development: Make. A. Game. With. It. No excuses. I'm looking at you. This is not a metaphor. Yes you. You know who you are.

So, just so you can get an idea of all the cool little things I built into this language and we didn't cover yet, the next section is going to be all about this dogfooding: Let's break tradition and, instead of hello world, look at some non-toy real world usage examples (as real-world as my silly game is anyway :blobcatgiggle:).

A Primer In Rebelry

Much like in real life, more than names, I like to think that entities accumulate a collection of monikers. Words that other beings use to describe them. After all, if other entities were to know my real name (not the one in human "legal records", mind you) they'd hold power over me. Programming languages are a lot like that. My language was called Rebel. But it being too (intentionally) cringe of a name—sometimes even for me—most people referred to it as 🥕. For accessibility reasons, even though I'm told most screen readers would speak out a normie-moji just fine these days, this is a carrot emoji. So the monikers this language accumulated during its life included 🥕, 🥕lang and carrotlang. Oh, and of course, avoiding to name one's creation: "the language" which is one of the guiding principles of the Jonathan Blow school of language design.

Anyway, I hope you're not here to discuss names. Let's discuss code snippets! :akko_fistup:

struct Sprite {
    tex_name: Option<String>,
    tex: TextureHandle,
    frame_idx: i64,
    next_frame_time: f32,
}

struct SpriteDrawParams {
    // The position in the world to draw the sprite.
    position: Vec2,
    // The alignment of the sprite origin
    align: SpriteAlign = SpriteAlign::Center,
    // Defined as an offset from the origin. The point in which the sprite
    // rotates around and gets scaled from.
    pivot: Vec2 = Vec2_zero(),
    // The color tint to apply to the sprite.
    color: Color = WHITE,
    // The size of the sprite.
    size: Vec2,
    // The rotation of the sprite in degrees.
    rotation_degrees: f32 = 0,
    // Whether to flip the sprite horizontally.
    h_flip: bool = false,
    // Whether to flip the sprite vertically.
    v_flip: bool = false,
    /// The z_index to draw this sprite on
    layer: Layer,
}

Ah, sprites, the heart of every game. I wanted to start with this snippet because it showcases a few things I really like about the language. Let's get the boring out of the way first: Yes, this looks like Rust syntax. You'd be forgiven for thinking this is Rust if you've never written Rust. That's the point: syntax is uninteresting to me. If you start discussing syntax with me, I'm going to pretend I'm listening because I'm polite, but I'm actually singing this obscure sonic green hill lyrics youtube video inside my head. Now onto more interesting bits.

One thing I really like from Rust is the idea that structs have a set of fields, and the only canonical way to construct those structs is by invoking the only one constructor that requires initializing. every. single. field, no exceptions. This is why you don't see any "constructor" definitions here or things that look like it. As you may expect, the way to initialize these structs is using syntax like this:

let s = Sprite {
    tex_name: <something>,
    tex: <some_other_thing>,
    frame_idx: 0,
    next_frame_time: 0
}

Unlike Rust, though, you can see I relaxed the rules a bit: Look at all those default values for fields! v_flip: bool = false, because 99% of the time you won't want to flip things vertically. 🙃 Constructing types in Rust sometimes is painful. There is a catastrophical combination of lack of function overloading plus lack of default arguments, both in functions and types declarations themselves. All of this leads to a proliferation of functions called new, new_with_this, new_from_that. Or perhaps even worse, some avid redditors dusting off the gang of four book and summoning the Builder pattern to justify their life choices. None of that nonsense here. If a field has a default value, then you can omit it when constructing the type. Does that mean my language has incredibly advanced const-evaluation machinery that lets me put anything in there? Ahahaha nope :blobcatgiggle:. I just do the obvious thing nobody seems to point out in these arguments. If I have a field in a struct initialized like this:

struct Foo {
    thing: String = call_a_fucking_template_engine_over_http_for_all_i_care(),
}

Then, the expression on the right hand side gets automatically inserted at every construction site and deterministically evaluated when the struct is constructed. It's actually not that hard.

Other minor things to point out are the types, you have the usual primitive suspects like f32 and bool, you'll notice custom user types too, and there's even our favorite burrito-shaped type, the Option<T> in there!

People who have not suffered in :ferris: prison won't bat an eye to that String in there, they'll probably bundle it up logically to all those other primitives. But for those who have seen the horrors, we know strings get messy. So let's focus on strings. There, have a string snippet. :akko_smile2:

impl StatUpgrade {
    fn apply(self, stats: Stats) -> Stats {
        let old_val = stats.get(self.kind);
        stats.set(self.kind, self.op.apply(old_val, self.amount));
        stats
    }

    fn describe(self) -> String {
        let increase = match self.op {
            Add => $"+{format_f32(self.amount, 1)}",
            Multiply => $"+{format_f32(self.amount, 1)}x",
        };
        $"{increase} {self.kind.describe()}"
    }
}

Stat upgrades, now that's some real world example. Most gamedev examples don't show "stats", because people who write about gamedev focus on having a character you can move around in an empty field, not in designing a level progression system that's actually engaging. We're not doing that either here, but let me tell you: That is the real nightmare. Designing programming languages for a bunch of nerds is easy next to designing an arbitrary set of rules that is fun, but also easy, but also challenging enough, but also rewarding, but also satisfying... :pensivecat: I'd like to think I've gotten better at this, but the more you get into game design, the more you realize it is by far the hardest part of developing a game.

Anyway, let's get back to the cozy world of language design... Strings! In the Rebel world of the 🥕, there is only one opinionated String type. If your use case needs to differentiate between a dozen string types, you probably need Rust and not Rebel, and I fully support you. I admit sometimes the subtle distinctions make sense, but those never mattered to me when writing games. And while the can of stringy worms is open, I'll mention Rebel Strings are immutable, literals are interned, other strings are intern-able and there is proper string interpolation.

By proper string interpolation, I mean like the one in Python's f-strings or Javasript's backtick-strings. You know, not like the one Rust has where you can only type the name of a variable, but any "complex expressions" were deemed too hard to implement dangerous because Rust doesn't trust you to not abuse this power and write conway's game of life inside a string literal. I say you $"{watch_me() + do_just_that()} :)".

But I know what you're here for. You wanna take a peek at the horrors internals, let's shift focus into the standard library for a bit! :akko_fistup:

struct RawString {
    len: u64,
    ptr: RawPtr<u8>,
}

/// Builds a string from a pointer and a length. This includes allocating a new
/// Ref to hold the inner RawString
extern "stdlib" fn String_from_ptr_and_len(ptr: RawPtr<u8>, len: u64) -> String {
    let r : Ref<RawString> = Ref(RawString { len: len, ptr: ptr });
    // NOTE: This below is "poor man's transmute". This works, but is highly
    // unsafe. We are allocating a new Ref, and then converting it to a String,
    // which has the same layout, but changes the deallocation strategy.
    //
    // We then dereference that string and put them into a variable. This
    // creates a copy of the ref, increasing the refcount, wich we return.
    //
    // Before returning, `r` is dropped and the refcount of the returned string
    // goes back to 1.

    let s = &r as RawPtr<String>;
    let s2 = *s;
    return s2;
}

Wow, look at that wall of text of a comment. Ominous. But let us not concern with whatever scary thoughts past me was thinking back then, this was working, it was just a bit wacky. As you can see, these strings consist of a RawString struct: pointer plus length. No capacity, because Strings are immutable. The string is then wrapped in a reference-counted pointer, aka Ref because otherwise you'd have to deallocate it manually. So, except in name, and despite the ominous comment, a String is a Ref<RawString>.

You can't see that here, but Rebel has a runtime, and that runtime is written in Rust, so Strings are allocated by the same allocator as Rust. That means you can cheaply throw strings over the fence to the Rust side and have them behave as &str through some RebelString wrapper type. You couldn't easily morph them into a Rust String though, because of the immutability, but other than that, our strings play nice with the crab!

Did I also mention our strings play nice with the C crowd? :grug: Strings have a length, they're civilized strings that enjoy drinking caramel chai latte and go shopping for cute outfits. But they're also nul-terminated. That nul terminator wastes a single byte per string: "Preposterous!" I hear you cry, but this makes it so not only Rust interop is nice, but C interop is nice as well. If a C API, say Raylib's—to name one C API I'd like to use—takes in a const char*, chances are its Rebel wrapper can take a String at no extra cost. Now this is a decision I made, but I'm also not super attached to it. You may feel strongly against nul terminators in strings, and you can anticipate my reply as: "sure, whatever, just don't have a nul terminator then".

I'll indulge into a bit of syntax discussion now, and note how you may have noticed the RawPtr<T> construct there. I believe if there's one thing I care about in syntax, it's consistency. We don't need special syntax for pointers, especially not if it means they look like *T and *mut T, we also don't need arcane syntax for things like slices &[T] or whatever else you can cook up. There's sigils and there's types, and you don't mix the two so that "thing plus sigil" is also a type. Everything is better and more consistent—especially when it comes to making things easy for metaprogramming—if constructs in your language are consistently named. A raw pointer is a raw pointer to something, right? We know how to spell that out, it's RawPtr<T>, no special syntax. Just like when we mean a vector of something we type Vec<T>.

But let's leave the low level lands and move onto more interesting things, this is not a low level language after all. Did you notice that match statement two snippets ago? Does that mean...

enum ControllerId {
    Keyboard,
    Gamepad: u64
}

impl ControllerId {
    @operator(==)
    fn eq(self, other: ControllerId) -> bool {
        match self {
            Keyboard => match other {
                Keyboard => true,
                _ => false,
            },
            Gamepad(id) => match other {
                Gamepad(id2) => id == id2,
                _ => false,
            },
        }
    }
}

Enums! Or discriminated unions! Or whatever the folks up at the ivory tower like to call these! :akko_fistup:

No surprises there, since I mentioned ten thousand words ago that I like discriminated unions when they're paired with exhaustiveness matching. You can infer the existence of those todo-list-generator exhaustiveness checks at play here from the wildcard _ cases.

In another manic burst of simplification, I decided to do away with the old cruft, and remove the useless inline struct and tuple variants of rust enums. Instead, enums here look more like a discriminated union: Each variant has a tag (Keyboard, Gamepad), and variants can optionally have a payload (in Keyboard's case, a single 64-bit unsigned integer). This payload can be as complex as you want, it could be its own struct, or a tuple! The difference here is that of orthogonality. Most people dislike this aspect about Rust enums but often focus on the wrong words to describe the feeling. The problem is that Rust lets you define enums like this:

pub enum SomeEnum {
    // Field-less, just the tag
    Variant1,
    // One-element tuple, the variant has type `A` as its payload
    Variant2(A),
    // Multi-element tuple, the variant has multiple types as its payload
    Variant3(A, B, C),
    // Inline anonymous struct payload, the variant has a sort of "named tuple" as a 
    // payload, where fields have names and each name has an associated value, like 
    // a struct, but it's not a struct.
    Variant4 { foo: A, bar: B }
}

To be clear, what I removed is variants 3 and 4, and kept 1 and 2 as the only options. Then I shifted syntax a bit so enum definitions are consistent with structs using : to separate variant and payload. The benefits of this, beyond mere consistency, is orthogonality (that word again!). Type definitions are type definitions, and an enum is a single thing where each variant has either zero or one type definitions. No special rules, no magic anonymous structs you can't name the type of. You'll often hear rustaceans ask why you can't declare that a function takes in a specific variant of an enum. Even though this is one of the least uncontroversial wishes in the Rust community wishlist, it hasn't happened yet because the rules around that are so complex it would take a huge effort to get it right. You'll often hear people recommending you stay clear of variants 3 and 4 and stick to variant 2, declaring a struct for each payload. That is exactly what I did here. That way, if you want a function that deals only with gamepads, what you do is pass in the gamepad index (the payload), and not a ControllerId. Wrap that u64 in a GamepadIndex newtype if that makes you feel less uncomfortable, because if you're worried about that then you just missed my point.

Now I'm already hearing you cry: but what about the convenience? Look, I've spent 10 years writing clojure for fun and profit, so I have grown to appreciate deeply nested un-flattened type definitions too. Sometimes traveling outside your little bubble gives you ideas, and when you look at things from a different angle, you realize what Rust needed here was not five more different ways to define a type, but merely a bit of syntax, just a trick of the light: A fuckery. Let's make our enum a bit more contrived than what was actually needed so I can make my point:

// This is the flattened version you see in Rust
struct KeyboardId {
    RawPtr<u8> some_sdl_shit;
}

struct GamepadId {
    index: u64;
}

enum ControllerId {
    Keyboard: KeyboardId,
    Gamepad: GamepadId,
}

// But you could write it like this as well in Rebel
enum ControllerId {
    Keyboard: struct KeyboardId {
        RawPtr<u8> some_sdl_shit,
    },
    Gamepad: struct GamepadId {
        index: u64,
    }
}

See? Orthogonality, that's what I'm talking about! The nice thing about this, is that inline type definitions are no longer an ad-hoc thing that is restricted to some corner inside of enums. Anywhere a struct expects a type, you can just type in the type definition, right there and then. If your compiler is any good, then order of definitions does not matter, so you can nest definitions like this, and it would be as if you had typed them out one after the other, with the added clarity introduced by nesting.

However, because this is a syntax thing, I must confess I never got around to implementing it. Because typing out the flattened version never bothered me too much. But the idea is sound and implementation is quite trivial actually, I just never had a pressing need for it... This was actually one of my guiding principles throughout this adventure. My brain may feel very proud of this idea, but if I never bothered implementing it, maybe that should tell me something. One thing is true: I sure as hell never needed the inline anonymous structs and tuple variants Rust has either.

You may be pleased to hear I also had plans for yet another orthogonal feature for the language, anonymous structs. It's pretty much what it says on the tin. Just the idea that anywhere where you could write a type name, you could write a little struct definition without name, and that would be an anonymous struct, or named tuple if you must. And because of, say it with me again, orthogonality that same thing would be possible in enum definitions. So you can avoid one of the two hardest problems in computing. For the same reasons as I just mentioned, I never got around to implementing it, but I believe it's cool and would've eventually made its way into the language.

Anyway... Enough with features I never got around to implementing. I hope you didn't miss that @operator thing up there, because that's coming up next!

impl Vec2 {
  @operator(+)
  fn add(self, other: Vec2) -> Vec2 {
      Vec2 {
          x: self.x + other.x,
          y: self.y + other.y,
      }
  }

  @operator(-)
  fn sub(self, other: Vec2) -> Vec2 {
      Vec2 {
          x: self.x - other.x,
          y: self.y - other.y,
      }
  }

  @operator(*)
  fn mul(self, scalar: f32) -> Vec2 {
      Vec2 {
          x: self.x * scalar,
          y: self.y * scalar,
      }
  }

  @operator(*)
  fn mul2(self, other: Vec2) -> Vec2 {
      Vec2 {
          x: self.x * other.x,
          y: self.y * other.y,
      }
  }
}

You know, I put my money where my mouth is. If something was up there in that first chapter, I sure as hell focused on it. Did you hear me mention a trait system as an essential language feature? No you didn't. :akko_smile2: Did you, on the other hand, hear me mention operator overloading? Yes, yes you did! And Rebel has those.

I don't dislike Rust's idea of operator overloading: Tying each operator to a trait so things implementing the trait support the operator is actually great. I just didn't feel a trait system was the right fit for a gamedev language. Traits are there so you focus on the generic and the abstract, and when making games you want to avoid all the associated rabbit holes that come with thinking abstractly. Games are very concrete, and so are their typing needs. Any generics you'll ever need look more like template replacement, and so it's better to just take the simpler approach and embrace templates. Templates are great when you keep them simple, and if I ever hear you mention the word SFINAE I'm muting you for three weeks, because I didn't mean that.

So, what I did was simple, but I believe quite elegant. Stick @operator on top of a method, and that method becomes an operator overload. The method will still exist as a regular method, so when you're getting all meta, you can refer to the operator by using its name. But what happens if you want to have a way to multiply vector with scalar, and you're some sort of degenerate that believes think it also makes sense to multiply a vector with a vector? Well, you annotate two separate functions with @operator(*) and they'll become two separate overloads for the * operator. The language will make sure things are consistent, and there is no orphan rule of any kind to stop you from bolting operators on a foreign type... That is, if the language had any notion of "foreign" in the first place. I never added a module system.

Just to hammer that previous point even further: There is no orphan rule. None, whatsoever. The language got its name for a reason. It is not called WellBehavedLang, it is not called ResponsibleLang. It is not called "I am the maintainer of a popular open source library and I spend my free time reviewing pull requests"lang. If you can't sleep at night thinking someone might implement an operator, or a method, for your pristine type you carefully designed and break your invariants 👻, you're in the wrong mindset. This is a language for small tight-knit teams of competent people that respect and trust each other. If coherence is accidentally broken, you'll see an error, and you can yell (/affectionate) at whoever screwed up because they're right there beside you: Either physically, digitally or through some sort of plural system interaction.

Now, you know what's something a rustacean hates more than breaking coherence rules? :blobcatgiggle: Global variables. So let's look at that next.

#def_game_data(Character)

global CHR_DRIFT_CAR: CharacterKey = add_character(
    "chr-drift-car",
    CharacterData {
        name: "Drift Car",
        name_lines: [
            "Drift Car",
        ],
        short_desc: "I have a feeling I have been in this place before.",
        tex_name: "drift-car",
        death_text: "I'm out of gas, gas, gas...",
        stats: Stats_new([
            StatValue_new(StatKind::HitPoints, 100.0),
            StatValue_new(StatKind::HealthRegen, 0.1), // hp per second
            StatValue_new(StatKind::MovementSpeed, 4.0),
            StatValue_new(StatKind::PickupRadius, 3.0),
        ]),
        starting_weapons: [WPN_CIRCULAR_SAW],
    }
);

Woah, so much to unpack here... but let me start at the less obvious and direct your eyes at those string literals: Notice the lack of .into() or String::from surrounding those? :akko_smile2: Remember: There's just one string type. You may have almost missed it, but look at stats:, is that a list? Surely such a convenient syntax would be reserved for initializing an almost useless stack-allocated arr... what? That [] is initializing a heap-allocated Vec<T>? Yup. Let's not turn this into a syntax argument, but I like it when the language is lean enough that I can encode my game data into a set of language literals. Sometimes it stays that way all the way until release, sometimes it's moved into external data files as needs arise.

But with that minor thing out of the way... look at it. I'm sure you've seen it. Equally glorious and disgusting. You know what I'm talking about, that global keyword. It is piercing into your mind, sending chills down your spine, in a way you never thought was possible. Very few languages have the boldness to introduce a global keyword, because everyone universally agrees that globals are bad, they just don't know how or why, but somebody told them so it must be true. All of those languages also support globals, because globals are actually useful and necessary, they just hide them under a euphemism, like static or the "singleton pattern". Some of them are more unhinged than I am and make all variables global by default, I'm looking at you Lua, huge respect there. But also: What the fuck?

Globals are fun. There are problems with globals of course. Everyone knows you should use dependency injection instead so that you can stub your main systems to run in unit tests. But we're not here to climb up the ranks in the corporate ladder, we're here to have fun, and we're here to get shit done. In that sense, a global is like a stick: The ultimate tool, endless possibilities, and it's been there since the dawn of humanity, but that doesn't mean we've stopped using it. We use sticks all the time (at least I do :ablobcatnodfast:).

In Rebel, globals are named for what they are, and because it's a language built with hot reloading in mind, hot reloading doesn't break with globals, but gives you the tools to work with them. For instance, you can tell the runtime whether a global must be reinitialized on each game reload or not. Why is that useful, I hear you ask? Well, the global has two parts, the type definition and the initializer. As long as the type definition doesn't change, you can keep safely reloading the game. Sometimes you'll have a global that holds your list of enemies on screen: You don't want that to go away across a reload and reinitialize back to the empty list! On the other hand, for the snippet above, it'd be quite rad if you could edit your character's stats right there and see the changes in real time. Well, that's what Rebel did. A @persistent annotation in globals lets you define whether the contents of globals survive a reload or whether their initializer function should be invoked again.

There were some rough edges I never fully ironed out, but it was convenient and it was fun to work with. Rust makes working with globals almost universally a painful task. If you dare so much as to utter the words static mut, you have a security advisory at your doorstep. The other way to do globals is via the world of fun introduced by having to type things like OnceCell<Arc<RwLock<T>>>, and all the associated pain that comes with it. It's not that I don't see the value in these things, mind you, but my globals are just as safe as Rust's and they don't make my life miserable.

About those rough edges, well: When globals can be initialized with whatever (remember, there is no const here, any expression is a valid global initializer), you run into the problem of a global being initialized by dereferencing another global. I never solved this in the general case (as in, if a global accesses another global through a function call), but at least I did in the basic case. For the remaining cases, I added a hacky @priority annotation so I could control initialization order. Not the best if you ask me, but it did the job. I had a to-do item on that kanban board to rethink all this global initialization logic.

There is one thing in that snippet I didn't comment on. That #def_game_data at the top, what's up with that? Well, for all my talk about metaprogramming, when you're in charge of writing the compiler, you have so much power and there is so much to do that I didn't have time to add a lot of metaprogramming features. Still, what you're seeing there is Rebel's rudimentary macro system at play! Let's take a look at that:

macro #def_game_data($name: Ident) -> ItemList {
    #let($Data, #concat($name, Data))
    #let($Key, #concat($name, Key))
    #let($GLOBAL, #concat($Data, _GLOBAL))

    // Define the AMap for this type of game data
    #def_amap(String, $Data)

    // This global will store all instances
    @priority(100) global $GLOBAL: #concat(AMap_String_, $Data) = 
        #concat(AMap_String_, $Data, _new)();

    // The key can be used to refer to a piece of game data.
    struct $Key {
        key: String
    }

    // add_game_data: Registers a new instance of the game data type
    fn #snake_case(#concat(add_, $name))(name: String, data: $Data) -> $Key {
        $GLOBAL.insert(name, data);
        $Key { key: name }
    }

    // all_game_datas: Returns all instances of the game data type
    fn #snake_case(#concat(get_all_, $name, s))() -> Vec<$Key> {
        let keys = $GLOBAL.keys();
        let out : Vec<$Key> = [];
        for i in 0..veclen(keys) {
            push(out, $Key { key: keys[i] });
        }
        out
    }

    impl $Key {
        // Returns the game data instance behind this key
        fn get(self) -> $Data {
            $GLOBAL.get(self.key)
        }

        @operator(==)
        fn eq(self, other: $Key) -> bool {
            self.key == other.key
        }
    }
}

What have we got here? I hadn't looked at these in a while, and I must confess I feel very proud of myself for coming up with this. :ablobcatnodfast: The idea here was to have something that sits in between Rust's unwieldy macro_rules and the pile of hacks that the C preprocessor is, with a sprinkle of Lisp on top.

Now, it's weird that, after all that talk about metaprogramming in the previous chapter I go on and implement the one single thing I didn't care about. This is the most plain and boring kind of metaprogramming: Token substitution. But I did it out of necessity. This was easier to implement for me than generics, and it proved just as useful, if you can excuse typing ThingSet_new() instead of Set<Thing>::new().

Let me start by quoting myself and saying when it comes to hygiene, you should focus on taking a shower from time to time and leave my macros alone. :akko_smile2: This is why my macro system was anything but hygienic. I ask you to behold the gloriousness of #snake_case(#concat(get_all_, $name, s)) and then take a minute to ponder about the fact concat_idents! in Rust being both unstable and not even as half as useful as this. Look at what these folks need to mimick a fraction of our power. In case it's not clear, what this is doing is taking $name, e.g. Character from the first argument passed to #def_game_data(Character), and constructing the get_all_characters identifier, used to declare a new function. What happens if the user had already declared a function with the same name? See my point above about the orphan rule, languages that respect the competence of their users, small tight-knit teams that respect each other as intelligent human beings, and so on.

But there is a lot more to unpack here: Look at that #def_amap, macros calling out to other macros! I mean, it's not that surprising, but this sort of modularity is useful, and I made sure it works fine. Macros were their own little language, like compile-time functions, taking AST blobs and returning AST blobs, composing well with one another. Then the compiler provided some intrinsic ones like #snake_case or #concat for your delight. And don't forget about those #let! I find it very useful to be able to define reusable snippets of ast blob inside my macro so I can avoid repeating myself. All in all, this ended up being a lot more ergonomic to use than Rust's macro_rules, while still being a lot more safe (integrated with the compiler, works at the token level and not with string substitution) than the C preprocessor.

On a very unrelated tangent, what's that AMap thing the #def_amap macro emits, you may ask? Simple! It stands for array-map and it's the most dumb way you can implement an associative data structure: List of key-value pairs, linear scan on every lookup. Somebody told you years ago this was a bad idea, but I just love the simplicity of it and I wish more languages offered it as a standard data structure. Nothing against hash maps, mind you, I just like to have the choice sometimes, and these little guys come in surprisingly handy time and time again.

Back to the topic, if there even was one to begin with, I find the biggest asset of this macro system was its plain and boring procedurality. I never needed loops or if statements, but the interpreter was super simple and it would've been trivial to implement #if or #for. There is no puzzle here. It's not the sort of thing that would get your average Prolog enthusiast excited. It won't give you a chance to write an entire book on the topic with the most obscure cursed programming patterns. It is just yet another uninteresting stick you can use so you can scratch your own itch. I hope other people can join me in being excited about stick-like tech that gets the job done, because it feels lonely in here sometimes. :pensivecat:

The fact macros were its own little language, separate from the real language is actually a bit of a regret of mine. I'd rather have a lisp where macros are written in the same language you use to food your dog. I just couldn't figure out how to make that work without the parentheses so I did this instead.

I guess the only thing left I want to show you is the Ref. I've passingly mentioned it a few times, but it was truly the star of the show, and a great excuse to discuss some deeper semantics. Let's take a peek at the darkest corners of Rebel's standard library. :sparkletrans:

struct RefFatPtr {
    ptr: RawPtr<u8>,
    meta: RawPtr<RefMetadata>,
}

struct RefMetadata {
    ref_count: u64,
    is_static: bool,
}

fn Ref_get_meta_ptr(ref: RawPtr<RefFatPtr>) -> RawPtr<RefMetadata> {
    ref.meta
}

fn Ref_get_ptr(ref: RawPtr<RefFatPtr>) -> RawPtr<u8> {
    ref.ptr
}

/// Allocates a new reference counted object, with an initial refcount of 1 and
/// with a data layout of the given `size` and `align`ment.
extern "runtime" fn __rebel_alloc_ref(size: u64, align: u64) -> RefFatPtr;

/// Free a previously allocated reference counted object with the given layout.
extern "runtime" fn __rebel_free_ref(ref: RefFatPtr, size: u64, align: u64);

/// Increments the reference count of the given reference counted object.
/// Returns the new reference count.
/// 
/// NOTE: Only called when the debug option of use_ffi_for_ref_inc_dec is enabled.
extern "runtime" fn __rebel_inc_ref(ref: RefFatPtr) -> u64; 

/// Decrements the reference count of the given reference counted object.
/// 
/// NOTE: Only called when the debug option of use_ffi_for_ref_inc_dec is enabled.
extern "runtime" fn __rebel_dec_ref(ref: RefFatPtr) -> u64;

Now that's a mouthful :neofox_laugh_sweat:. To give you some much needed context, Rebel has two pointers types: RawPtr<T> is a raw pointer: No safety guarantees, be careful, you're on your own kinda deal. I never added unsafe to the language because I didn't need to, but if I ever did, messing around with RawPtr would've probably required it. So pointers aside, in Rebel, types are value types, and all types are passed around by value. So how do we get around that fact whenever we need to safely pass things around by reference? We use a Ref, that's how.

The Ref is Rebel's refcounted "smart" pointer type. The closest thing you could utter in Rust would be Rc<RefCell<T>> (remember, we're proudly single threaded), except here we (i) are not telling LLVM these references are exclusive via noalias and (ii) take proper care to guard against nasty situations like iterator invalidation in our safety model. So the RefCell is not really there, you just get to mutate aliased Refs however you want. And it's liberating.

Now we're ready to take a peek at the internals: What's in a Ref? A Ref is a "fat pointer", shorthand for "struct that holds two pointers". The first pointer points to some piece of allocated data, the second pointer points to the ref metadata heap, a location inside Rebel's runtime where the refcount is stored, as well as some additional metadata. The only metadata you can see here is that is_static thing, which is used to prevent things like interned strings from actually deallocating (remember, one string type, not twenty, so compromises have to be made).

The piece of magic Refs have, is that when copied, they're not copied like the other types (aka, blitted). Instead, the compiler makes sure to insert refcount increments and decrements whenever Refs are passed around: Either by themselves, or by being a field in a struct. The whole thing was reasonably performant too. Some optimizations were in place to ensure you didn't redundantly increment or decrement the refcount too much. LLVM added some more on top (I checked!), eliminating some bounds checks, because the refcount wasn't atomic (again, proudly single threaded :ablobcatnodfast:) and it could merge x += 1 with x -= 1 into a no-op. And the worst performance bit was to worry about the branching that occurred when a ref fell out of scope and we had to check whether refcount was zero so we could deallocate it. I slept at night thinking that branch was somewhat well-predicted, though not as well as others. I may be exaggerating here, but that branch instruction is probably the reason garbage collectors exist at all.

Let me tell you, figuring out when to increment and decrement those refcounts was hellish for someone like me who had barely scratched the surface of the dragon book. This required bringing out the heavy artillery. But I made it work. It was robust, I wrote a lot of code with it. By the end of it, I'm reasonably convinced there were no more bugs after months had passed with zero Ref-related incidents at Carrot Corp.

Implementing proper dataflow analysis algorithms on top of my own IR was by far the most technologically complex bit of this project. I didn't start this project to build something "impressive" or to satisfy my own ego. This whole thing was started to scratch a very itchy itch, one that was consuming me from within. If I could've handed this over to someone else and told them: There you go, please implement this complicated bit for me because I'm dumb. I would've done so gladly. I just wanted to make games.

And how fittingly! Because things are about to get a bit darker from here on. :neofox_laugh_sweat: In case my grammar choices regarding verb tense and the name of the blog post didn't give you any clues yet...








Rebel is the language that never was.








Wait, don't leave! I'm no impostor, despite the syndrome. All this code exists, and it is sitting there in my hard drive. I didn't make this up. All the snippets you've seen are real, they compiled, and they ran. It's just that... it is very unlikely that this little language sees the light of day.

Chapter 3

So What Happened Next?

Mental health. What a beautiful concept. Things are about to get personal next and I'm not used to communicating personal thoughts, but if you got this far, you'll figure it out.

Motivation And Lack Thereof

Every side project goes through phases. My employer at the time wouldn't be happy to learn how much time I put into this when motivation was at its peak. Or maybe they would, I've always been very lucky with my employers. :blobcatheart: This was about a year, and maybe a few months, of intense side-project work. I poured my soul into it. But motivation doesn't last forever. It was sudden, one day everything crumbled, and since that day, I haven't touched a line of code for this project.

I'd say focusing too much on the straw that broke the proverbial camel's back would be unfair. That's why I've taken my time before writing this blog post: It has now been a bit over a year since the whole Rebel project was put to a halt. I've taken my time to reflect, consider things, get my thoughts (and my shit) together, before putting any of this into words. But if you insist, I'll dig up the message I wrote at the time, in the heat of the moment, the straw that broke the camel's back, in all its glory. I do not stand by these words today, but these were my words back then:

You can thank the absolutely disgraceful state of wasm and anything that is related to it for killing my last bit of motivation of contributing to the Rust ecosystem. I hope they sort out their ABI issues some day, but it won't be me fighting to make it happen. I'm going to be inflammatory for once and say that the cryptowebshit bureaucrats behind the bytecode alliance can go redacted, things you really don't wanna read. (sic. as in, I never typed those things out, they were redacted in the original)

I don't even fully remember what this was about, nor did I take the time to dig out the relevant issue thread for the blog post. I had just finished adding the AoT mode to the compiler and I was trying to add webassembly support to the language so people could play my game on the browser. I was well on my way there, actually! Then I hit an issue with the Rust compiler not following the webassembly ABI when passing large structs as function arguments. The issue was a known bug, one that had a solution, but nobody could agree upon it. Things were stuck in RFC PR limbo. One thing led to another and there I was, trying to build my own fork of rustc, but failing, and then the fuse blew up and I was out of replacements. Rebel was dead, right there and then.

The real reasons that made me stop, which in retrospective had not that much to do with web assembly, come in next.

I No Longer Want To Contribute To Rust. The once vibrant, once thriving Rust community had shown some of its dark sides to me. A community, fueled by the perfect combination of newbie enthusiasm and veteran burnout. So many thankless volunteers holding it all together. Then a small few, who shall remain unnamed, reaping the benefits of the hard work. All held together by a layer of fanatics that systematically shun any criticism, valid, thoughtful or otherwise.

I'm being purposefully vague here since this is not about attacking any particular people, forums or projects. Chances are if you think this is about a notable Rust thing, it indeed is about it. But it is not just about that one thing you have in mind, it's about them all really. I was tired of it.

Just the fact the average Rustacean heavily advocates for an Apache2+MIT dual licensing model (sometimes they throw Zlib at it for good measure) and bullies projects who dare to go in another direction, especially if that involves any amount of GPL, should tell you something. If it doesn't... well, it did for me.

As a "contributor" of some sort, I had some notable open source projects I maintained after all. It felt like all the work I was putting into this was mainly for the benefit of people who, as time went, I disagreed more and more with. Disagreements on very fundamental aspects about how software needs to be produced and consumed.

You'd be forgiven for thinking this is a very average case of open source maintainer burnout because it was. My contributions were never that notorious, you probably haven't heard of them, or perhaps passingly, but they were there.

Rust Leadership Is In A Very Sorry State. Whether you are part of the Rust community or not, you've surely heard of all the drama. That juicy drama.

To some, especially anyone watching from the C++ side, all that must've been delightful. I can honestly see the appeal. :blobcatgiggle: No hard feelings. If anything, I'm taking a lesson here to never grow too attached to a programming language. Your choice of programming language should not be your only defining personality trait and I really hope it's not the thing that keeps you going in life. Get a hobby, start with gundam model kits if you're out of ideas.

Back on the drama. There were several incidents. There was the mod team resigning en bloc, the trademark policy lawyer fiasco... But the one I still resent was the whole RustConf thing. If you're not up to date, you can read Amos' mostly unbiased report of the events.

My relationship with JeanHeyd can be described as "omggg big fan!!1 :blobcatheart:" vs "who the fuck are you?". By that I mean, I think they're a really cool individual, one to whom I have no ties whatsoever. We've never even spoken. And yet, call it empathy if you must, whatever happened to them was painful enough for me to feel completely depressed about Rust's future. Enough to want me to cut all my ties with the language.

If you remember, back on the bright and beautiful pastures of chapter 1, I explained how metaprogramming is something I care a lot about. Not surprisingly either, compile-time reflection was at the very top of my wishlist for what the ideal gamedev language must have. They had the competent person, that person was going to make it happen. I was excited, very. Then egos were hurt (on both sides, but only rightfully so on one), and everyone lost because of it.

Now Rust will never have compile-time metaprogramming, and we're stuck in the wrong timeline. The one with the proc macros.

Even if Rebel was not Rust, it was meant to be "Rust's perfect companion" of a language, so whatever happened to the big cousin language affected me. Both my work and my motivation to continue it. And in all fairness, if you look at the dates, this particular event didn't kill my motivation, it fueled it. But looking back, it also planted that seed of dread that ended up destroying what little hope I had left for whoever sits behind the wheel at Rust corp.

Making A Compiler Is Tough. Much More Than I Imagined. But you'd be wrong to think it was all about Rust, their corporate bootlicking and their dramas. Making a compiler is really tough work. I've already touched on some of it, but let me tell you: It was brutal.

Perhaps, if my goal was to make a language, things would've been different. But my goal was not to make a language, it was to release games using the language. And I'm a very pragmatic person at that. I enjoy a good challenge, but if my goal is to ship a game, fixing compiler bugs in the middle of development is not fun but a huge distraction.

As you can imagine, there were many bugs. The language was complex. Targeting LLVM was no easy feat, though I did it, and I learned so much by doing it. If I could go back and do things different, I probably wouldn't. I think my compiler-making fuel is simply exhausted.

Because perhaps, the most important reason, one that deserves one of these big and impactful headers, perhaps, is that...

All I wanted is to make tiny silly games

That's it. I never set out to make a programming language, and I still don't know what got into me. It sure was fun, it made me feel amazing. The high from seeing that string of characters you typed into a file come alive on the screen. The feeling of being the creator, every decision up to you. No RFCs to type, no people to convince... That was quite something.

But at the end of the day, every day spent working on the gamedev language compiler was a day not spent on the game. That's how I saw it, and that's how I still see it.

Making something others want to play is a skill on its own, and it's a very different one to programming. I even dare say, the better of a programmer you are, the worse of a game developer you will become. You need to unlearn a lot of things. In solo game development, the inner game designer leads. The inner artist suggests. And the inner programmer shuts the fuck up.

In that sense, working on a programming language to develop videogames was very incompatible with my philosophy. The amount of cognitive dissonance this was causing over the year and a half or so I spent of this project took a toll on me. I just have this urge to craft experiences others are going to play, I don't know why. I'm also not that good at it, but I sure as hell won't get better at it by messing around with parsers and LLVM.

So, there I was. The language I spent so much time on, dead in the water. No chance of taking up the project again, burnout is not something you can recover from by doing more of what caused it after all. And a half-done game... wait, a game! I was working on a game. Remember the carrots? At first, it was this fun little side-side project, something meant to dogfood the language. And I sure ate lot of dog food. But I was having fun with it: If I had to finish one thing, I wanted it to be the game, not the language. But how do I finish a game without a language?

Bill Gates Enters The Room And Everybody Leaves

Remember that eerie ominous feeling, that chill down your spine? Back in chapter 1? Well this is it it: it's Microsoft. How the fuck did Microsoft end up in this story?

They keep saying, and nobody but them believes it (rightfully so), that Microsoft ❤️ Open Source. Well, they did one thing. You may not have gotten the memo, because who even cares about fucking .NET, but they open sourced .NET well over 10 years ago. Before that, corporate used to be all about the little Duke, :duke: but now there was a new kid in town.

.NET is a bit of a weird technology, in that it is as technologically impressive as it is widely ignored. I think the bad press its parent company earns it is the main reason for it. I'm not blaming anyone for that. Originally, it was tied to Windows and its ecosystem. That hasn't been true for a while, but nobody cares. It's that Windows thing, made for Windows and Windows only, and who on their right mind would want to wear a suit and tie to work, to work using what's essentially Microsoft Java?

While I was working on Rebel, I heard about the news of dotnet hot reload being a thing. I chose to ignore them, because if true, my whole world was in danger. It's something the brain does to you, you ignore the things that challenge your world views because thinking about them causes pain. It causes what fancy people call cognitive dissonance (that word again!). Back then I was fueled by the motivation that no low level language (low level enough to support the value types and raw pointer fuckery I needed, anyway) cared about hot reloading. Because if I had looked, even just for a bit, there I had it: Microsoft, the bad guys, telling the world that hot reloading is okay to want and possible to achieve. In the meantime, the rustaceans, mocking me for even suggesting the idea and deeming it useless, unsafe and nonsensical.

I spent the previous section enumerating all reasons why I stopped working on Rebel, my compiler, but the truth is the very same day I wrote that last line of Rebel code and furiously typed that quote, I asked a friend to share his proof of concept of C# hot reloading on raylib. I downloaded it, typed dotnet watch on my Linux terminal, some circles and lines popped into the screen. I modified the code, saved my file, and right there and then, the rebellion ended.

So what did I do? I don't like reading documentation without a purpose. I started porting Carrot Survivors to C#, just to evaluate if it'd be a good fit. I am not exaggerating when I say that the full game (and there was quite a bit of it!) took me exactly one week to port. After that week, I was productive with the language, had a working build and was adding new features to the game.

I had never seriously looked into C#, other than an occasional run-in with Unity back when I was a teenager still convinced that "all programming languages are the same". So I was both surprised, delighted and terrified to learn about how many of my boxes this thing ticked. I called C# depressingly good. I still do. :pensivecat:

Hey, let me just break the bullet list for a moment to hammer in this point, because it is important. This quote right here: Read it. Read it fully:

Compilers build a detailed model of application code as they validate the syntax and semantics of that code. They use this model to build the executable output from the source code. The .NET Compiler Platform SDK provides access to this model. Increasingly, we rely on integrated development environment (IDE) features such as IntelliSense, refactoring, intelligent rename, "Find all references," and "Go to definition" to increase our productivity. We rely on code analysis tools to improve our code quality, and code generators to aid in application construction. As these tools get smarter, they need access to more and more of the model that only compilers create as they process application code. This is the core mission of the Roslyn APIs: opening up the opaque boxes and allowing tools and end users to share in the wealth of information compilers have about our code. Instead of being opaque source-code-in and object-code-out translators, through Roslyn, compilers become platforms: APIs that you can use for code-related tasks in your tools and applications.

Holy crap. Oh how I fucking wish this had been written by someone at the Rust core team. But no, this is a verbatim quote from the Microsoft documentation. In the meantime, the people behind Rust can't even conceive the idea of providing a stable API to access the AST so that people don't have to parse the syntax tree twice and kill compile times. And I'm just taking a stab at the crab because that's the basket where I used to put my eggs. Pick any hip and trendy language you're excited about and read this quote again. It will make you feel as bad as it made me feel. Anyway, back to the bullet points...

The rest of the things in that first chapter though... well, they're missing. There is still hope, C# isn't the perfect gamedev language! Let's see, for it to be our ideal language we're missing *checks notes* :bunhdthink:

And... that concludes the list. It's fucking depressing if you ask me, that's what it is. Microsoft, of all companies, made the almost-perfect gamedev language while nobody was looking, and other than the Unity crowd we like to make fun of, nobody noticed... The whole C-replacement-wannabe tribe, with their heads up their butts focused on all the wrong problems, and Microsoft there, just catering to my interests like they had built the thing purposefully for me.

You could point out many minor papercuts you find annoying about C#. I could too. Having to write docstring comments in XML in 2025 is laughable, some operator precedences are wacky... I hear you. But I try to see the bigger picture: For me, C# ticked all the big boxes, Rebel did at the cost of my own sanity, and Rust ticked almost none. You shouldn't care so much about the small things, it's slowing you down. After all this, I have learned to excuse a little jank in my language choices as long as the important things I care about are there.

We should take one last tangent and discuss Unity, since I brought it up: Many of the things I've come to appreciate from C#, such as their excellent SIMD accelerated types in the standard library, most of the cool parts of their ref borrow checker and the juicy nullability checks that protect my foot from the null gun... They don't have any of that. Unity is stuck with an ancient version of dotnet, and dotnet (and C# with it) have come a long way in the past five years. Instead, what the Unity folks did was craft their own parallel universe, for better or worse. At this point, what Unity does, might as well be a hard fork, and the C# I'm talking about and what Unity users have are not the same language. Plus Unity sucks.

Anyway, I hope you now understand when I said it's depressingly good I meant it. :pensivecat:

If I Knew Anything About Marketing, The CTA Would Go Here

So... where does that leave us? Well, it leaves me with a released game on steam. Yes! I finally finished Carrot Survivors. It is now written in C#, with Monogame. Though I've decided to part ways with Monogame since (different story, this post was long enough). Go play it! There's a free demo, and people have told me it's fun. :ablobcatnodfast:

But please don't think of this post as an advertisement for my game, because it would be a very bad one. I enjoy making games and sharing them with others. This was my first release and I'm hoping to do so much more now that I'm finally focusing on what matters to me. I just think after reading all these words, you may want to learn a bit about who is behind them and what truly motivates me, you know. People don't wonder that often enough with blog posts. :akko_smile2:

Though I must admit all this leaves us with a bit of a bittersweet ending. The rebellion is dead, the bad Microsoft empire won, and our little language will never see the light of day. :/

I'd be genuinely happy if someone took all these ideas I put out there and made something out of them. I'd like to think my set of requirements and design ideas are interesting to at least maybe one other human out there? But I also don't have high hopes. Plus, the situation is dreadful. Unless you take your principles very, very seriously, you're probably using at least one of Microsoft lang (or this one, or this one), Microsoft squiggles, Microsoft Git, Microsoft Slop or Microsoft Emacs. I didn't even have to bring Office into the list, you should at the very least consider LibreOffice, I'm begging.

I truly hope someone does something before all the little rebellions out there are embraced, extended and eventually extinguished.

After all this journey, I don't know what to tell you. I started typing this post thinking I'd open source the language, the community would pick it up and it would live on: Further than I could ever take it. A grand finale. But after typing all this, I've realized things don't work that way. I'm tired of it, and I mean it when I say I no longer want to contribute to open source. Especially when it comes to anything Rust. As sad as it may be, Rebel stays on my hard drive, but the ideas live on. And the contradiction I now have to live with is hating open source because the people who made the language I love killed it. There is some burnout in all of this that is still healing.

What drove me to write all these words was looking for closure for this little project, and I found it. I'm thankfully in a better spot now. For what it's worth, I never was in a bad spot throughout any of this, though some of these feelings can get worse if untreated. To me, saying goodbye to Rebel and finally shipping my first game on Steam was therapeutic. 😌

And you, you've reached this far. Have some music, it soothes the soul. I promise it's not the broken Green Hill Zone theme this time. This one's actually beautiful.

Now, if you'll excuse me, I have more game to dev. :akko_fistup:

A short looping clip of a videogame.
The aesthetic is pixel art, but it is notably higher quality than the one you've seen
before. Since you're reading the alt text, it means you're cool, so I'll let you in on a
little secret that is hidden to the eyes and tell you this is a gif of my next project,
still not announced, and called 'Game 2: The Return of Game' for lack of a better
moniker. In the gif, you can see a windy landscape, with blades of grass, trees, a stone
wall and a pink-haired girl standing inside of the wall, holding a sword in an idle
athletic pose. People have told me it looks really good, but I wouldn't say that about my
own game. Anyway, thanks for tuning in to the alt texts. You rock! ❤️