Skip to content

Commit

Permalink
Release most of the backcompat series
Browse files Browse the repository at this point in the history
I am sick of having to repeat the bulk of this, so I am publishing
most of it right now. I will finish Part 5 at some later date.
ISSOtm committed Jan 29, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 9e354d1 commit c3e9315
Showing 10 changed files with 697 additions and 0 deletions.
43 changes: 43 additions & 0 deletions content/blog/2025-01-28-backcompat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
+++
title = "Why RGBDS has been breaking backwards compatibility"
authors = [ "stuff@eldred.fr (ISSOtm)" ]
date = 2024-12-20

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "RGBDS back-compat"
section = "blog/backcompat"
+++

<p style="--pico-color:var(--pico-muted-color);">Clickbait title: <em>I Broke Your Code And I'm Not Sorry</em>.</p>

We all hate when something we use, or rely on, breaks.
We hate it even more when it's someone else's fault.
I'm no different!

But then, why have I, as the lead maintainer of [RGBDS], been inflicting this pain upon other people for the past four years or so?

<!-- more -->

I am writing this post in the context of RGBDS, and more widely, [Game Boy development][GBDev]; however, the points made should be relevant to a wider audience: anyone designing tools or systems meant to be used by other humans or machines.
No programming knowledge should hopefully be required to understand any of these.
(Feel free to ask any clarification questions in the comments!)

## Executive summary

- Backwards incompatibility sucks.
- UX deficiencies also suck.
- Full backwards compat always has a cost.
- We have decided to prioritise UX improvements over back-compat.
- Nevertheless, we endeavour to reduce the pain as much as we can...
- ...but ultimately, we are short-staffed and not paid _at all_.
- Hope's not lost! There exist mitigations and workarounds you can apply to reduce the pain for yourself and/or other people!

[RGBDS]: https://github.com/gbdev/rgbds
[GBDev]: http://gbdev.io

---

This post has been split into several sections, to allow reading it piece by piece.
11 changes: 11 additions & 0 deletions content/blog/backcompat/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
+++
title = "RGBDS back-compat"

render = false
sort_by = "slug"

[extra]
stats = true
prev_text = "Previous part"
next_text = "Next part"
+++
254 changes: 254 additions & 0 deletions content/blog/backcompat/part1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
+++
title = "Context"
authors = [ "stuff@eldred.fr (ISSOtm)" ]

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "Context"
+++

The big deal with RGBDS and its backwards comaptibility is likely obvious to any seasoned member of [GBDev], but maybe you, dear reader, are not one.

Let's talk about what backwards compatibility is (in a rather general sense), and why it matters.

<!-- more -->

## What is RGBDS?

The short answer can be found [on its history page](https://rgbds.gbdev.io/docs/rgbds.7#HISTORY).
But, essentially, it's a collection of four tools that enables people to program their own games for the Game Boy, and which was initially released roughly at the same time as the Game Boy Color itself.

RGBDS was originally developed as a mostly single-person endeavour, but passed through several maintainers and contributors, as well as multiple platforms.
And, recently, RGBDS has seen releases more frequent than ever, bigger than ever... and also some compatibility breakage every six months on average.

<hgroup>

## Why is this a problem?

Or: "why is backwards compatibility desirable".

</hgroup>

<figure>

[![XKCD #2224](https://imgs.xkcd.com/comics/software_updates.png "Everything is a cloud application; the ping times just vary a lot.")](https://xkcd.com/2224)

<figcaption>An article about programming, written by a nerd, wouldn't be complete without some XKCDs~</figcaption>
</figure>

Due to the popularity of RGBDS within the niche of "Game Boy assembly programming", over the years, quite a lot of code relying on it has accrued.
Most of that code is either maintained by someone who uses their local copy of RGBDS, or is abandoned.
It has turned out that backwards compatibility breakages affect both of these categories.

See, the maintainers of active codebases have complained that when they want to upgrade (to benefit from a new feature or from a bugfix), they have to also adapt to the breaking changes from the new version.

And, meanwhile, people interested in checking out some of the older codebases have been faced with the problem that the latest version of RGBDS just spewed a bunch of errors... and they had no idea which version would be the right one&mdash;for those who realised that using an earlier version would work, in the first place!

So, given these clear incentives for RGBDS _not_ to break backwards compat, why has it been repeatedly broken for several years in a row?

<hgroup>

## The case against back-compat

Or: "how much do you _actually_ want back-compat?"

</hgroup>

{% admonition(kind="note") %}

Readers may disagree with me on the relative importance of each item, and their weight relative to the previous section's.
**This is expected**, as these are subjective and/or dependent on the project they apply to.

{% end %}

RGBDS was made by humans.
Humans are imperfect.

RGBDS was started back in 1997 as a single programmer's hobby project, and with seemingly only moderate knowledge of programming language theory.
Add to that that RGBDS changed hands several times through the years, still between hobby programmers, and it becomes fair to assume that some features were implemented without a lot of forethought.

Setting aside what works well, we are left with two categories:

- what just doesn't work,
- and what works but not well.

### Bugs bugs bugs 🐛

At first blush, fixing bugs seems uncontroversial.
<q>Oh no, Bad Behaviour! Who would want that? Let's fix it!</q>

<figure>

[![XKCD #1172](https://imgs.xkcd.com/comics/workflow.png "There are probably children out there holding down spacebar to stay warm in the winter! YOUR UPDATE MURDERS CHILDREN.")](https://xkcd.com/1172)

<figcaption>And yet.</figcaption>
</figure>

I like to point to one RGBDS bug report as a very good example of the XKCD above: [issue #362], "Labels can be defined with colons in their name".
Here is some code that triggered the bug[^old_syntax]:

```asm
MACRO mklabel ; Defines a macro, that, when called...
Label_\1: ; ...defines a label, "copy-pasting" the macro's first argument into its name.
ENDM
mklabel x ; Defines Label_x, perfectly legitimate.
mklabel a:b ; Defines Label_a:b, which shouldn't be possible!
```

Colons are _not_ valid characters for symbol names in RGBASM (labels being one kind of symbol), so this is definitely a bug.
[Worse still](https://github.com/gbdev/rgbds/issues/362#issuecomment-506028115):

> Any character that can be used in a macro arg can be used in a symbol name using this trick.
> For example, you can even put spaces in a symbol name.
Spaces in symbol names is a _huge_ problem, because it breaks the [sym file] format[^sym] that RGBLINK ends up emitting.
So, naturally, [the bug got fixed](https://github.com/gbdev/rgbds/pull/364).

In and of itself, fixing the bug broke compatibility.
But, since the behaviour wasn't matching the documentation, correcting the behaviour appeared much more sensible to us maintainers than altering the documentation!
Imagine reading a new tool's documentation, and stumbling upon:

> Symbol names can only contain the aforementioned characters, _except_ if they are generated through a macro argument precisely at the end of a label, in which case no guarantees are made \[...\]
Fixing this bug fixed the code shown above, and as usual after a bugfix, everyone was happier.
...that is, until [the following release][v0.3.9], when someone else reported that their previously-accepted code [was now rejected](https://github.com/gbdev/rgbds/issues/435#issue-502960245).
They were using code highly similar to the above, but with `$` as the "bad character" instead of `:` due to using another feature of RGBASM's.
Why didn't _they_ notice?
Well, it turns out that on other platforms (such as x86, which that user was used to), `$` is not only [permitted in labels][nasm-labels], but common![^dollar_ident]

The user could update their code to no longer rely on the bug, but this would have required significantly increasing its complexity, so they refused.
Instead, we agreed upon [a compromise](https://github.com/gbdev/rgbds/issues/2700), which would give them a way to stop relying on the bug with only minimal changes.

This was not an isolated case, either:

- When `ds -N` was removed, heated discussion took place in [issue #190] ("Disallowing negative constants in `ds` breaks backwards compatibility"), before being solved by a new feature in [PR #193] ("Add support for unions");
- Likewise, [issue #136] ("Section ordering change was not backwards compatible"), was solved by a new feature designed in [issue #137].

All of these perfectly illustrate [Hyrum's Law]:

> With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
Unfortunately, "all observable behaviours" **includes bugs**.
Yet, in the interest of having reliable software that performs as documented, I prefer fixing at least _some_ of these bugs.

[^old_syntax]: Actually, `MACRO mklabel` was invalid [at the time][v0.5.0], and [you had to use](https://rgbds.gbdev.io/docs/v0.3.8/rgbasm.5#MACRO~2) `mklabel: MACRO`. This blog's code highlighter only knows the former syntax, though, so I cheated to keep it aesthetically pleasant.

[^sym]: Actually, this specification didn't exist yet at the time of this bug report. But the format was almost exactly the same.

[^dollar_ident]: Amusingly, RGBDS v0.9.0 made `$` valid in identifiers for that reason; so, five years later, we have come full circle 😆

### It Sounded Better In My Head

After "things that don't work", let's discuss "things that work but not well".
These are often called "_papercuts_", because they're not "lethal" to one's experience... but still at least an annoyance.

A well-known example of a "papercut" is C++'s "[Most Vexing Parse]":

<figure>

```cxx
struct Horse {
std::string name;
unsigned int age = 0;
};

int main() {
Horse cadey("Cadey", 26);
puts(cadey.name.c_str());

Horse baby("???"); // Default value used for `age`, here 0.
puts(baby.name.c_str());

Horse anonymous(); // Default value used for `name` (empty string) and `age`... right?
puts(anonymous.name.c_str());
}
```
<pre><samp>main.cpp: In function 'int main()':
main.cpp:17:24: error: request for member 'name' in 'anonymous', which is of non-class type 'Horse()'
17 | puts(anonymous.name.c_str());
| ^~~~
</samp></pre>
<figcaption>
([Try this example yourself](https://coliru.stacked-crooked.com/a/28aea6316b300b7f)!)
</figcaption></figure>
The "papercut" here is that the "obvious" syntax _does not do what you expect_.
(The correct thing to do here is `Horse anonymous;`.)
This is also known as a violation of the [Principle of least astonishment] (or "POLA" for short).
Why are special cases bad?
Since we need to remember the tool's rules, what's just one more?
The first problem is that special cases increase "friction": they make \<the thing\> significantly harder to learn, because there's just more to learn; they _also_ make it harder to use, since you also have to keep in mind the general rule has an exception (and, sometimes, means that you must handle the exception specially).
They also increase the chance of mistakes (including bugs), because humans are _way_ better at sticking to rules than remembering exceptions.
(Also, this argument often comes from veterans who have gotten used to the papercuts, which makes it sound closer to ["Skill issue"] than I'd like.)
Let's give an example in RGBASM, with what's perhaps its most common papercut:
<figure>
```asm
MACRO fancy_println
println "✨ ", \#, " ✨"
ENDM
println "one"
println "two"
fancy_println "three"
fancy_println "four"
```

<pre><samp>one
two
✨ three ✨
error: main.asm(8):
syntax error, unexpected string, expecting : or ::
To invoke `fancy_println` as a macro it must be indented
error: Assembly aborted (1 error)!
</samp></pre>

<figcaption>

([Try this yourself](https://gbdev.io/rgbds-live/#LYQwlgdgdCDOwAACyBBAwgJQPIAIBmIEAxgJ4D6ADgE6QAuANhAFA47V2M4BEgFOTcA0OADoBiQVxw8uTAKIA5ACJImLNjQgMI3APYQAptPYbOXWgHdt01gWLkjm7rQAWVPQaY3SldQ655tAK5UXAhAA)!)

</figcaption></figure>

To be clear, the papercut here is that the built-in `println` directive can be indented and not indented, and that doesn't make a difference; but the similar-looking `fancy_println` macro _cannot_.
More broadly, you have two animals that quack like a duck, fly like a duck, swim like a duck... but one of them tastes like red pepper.

Let's do a brief aside on that ``To invoke `fancy_println` as a macro it must be indented`` line.
I have witnessed several users reacting to it with a frustrated "if it knows what I mean to do, why doesn't it just do it!?", and I understand.
The problem is that in _this_ case, RGBASM can infer what was intended; but in some cases, it can't, and thus the rule has to exist.
An analogy may help: if you were to mis-type `simklar`, your reader may be able to guess that you meant `similar`; but if you typed `dkg`, did you mean `dig` or `dog`? Or maybe `dug`?

## Summary

The key takeaway regarding backwards compatibility is that there is a fundamental tension between **keeping** what's currently working, well, _working_; and **changing** what's making life difficult.
Both aim to improve the user experience, but sometimes, the same change can improve some users' experience _and_ harm others'!

So, then, the natural next step is to ponder how handle changes.
The next part in this series compares various strategies, and weighs their pros and cons.

[GBDev]: https://gbdev.io
[dmgpage]: https://dmgpage.com/homebrew-releases/
[issue #362]: https://github.com/gbdev/rgbds/issues/362
[sym file]: https://rgbds.gbdev.io/sym
[v0.3.9]: https://github.com/gbdev/rgbds/releases/v0.3.9
[nasm-labels]: https://www.nasm.us/xdoc/2.16.03/html/nasmdoc3.html
[Hyrum's Law]: https://en.wikipedia.org/wiki/Hyrum's_Law
[issue #190]: https://github.com/gbdev/rgbds/issues/190
[PR #193]: https://github.com/gbdev/rgbds/pull/193
[issue #136]: https://github.com/gbdev/rgbds/issues/136
[issue #137]: https://github.com/gbdev/rgbds/issues/137
[v0.5.0]: https://github.com/gbdev/rgbds/releases/v0.5.0
[Most Vexing Parse]: http://en.wikipedia.org/wiki/Most_vexing_parse
[Principle of least astonishment]: http://en.wikipedia.org/wiki/Principle_of_least_astonishment
["Skill issue"]: https://knowyourmeme.com/memes/skill-issue-simply-a-difference-in-skill
Binary file added content/blog/backcompat/part2/breakage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
164 changes: 164 additions & 0 deletions content/blog/backcompat/part2/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
+++
title = "Dealing with mistakes"
authors = [ "stuff@eldred.fr (ISSOtm)" ]

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "Mistakes"
+++

Since there _will_ be mistakes, for one reason or another, let's explore the various ways they can be handled, and each's tradeoffs.

<!-- more -->

## Sticking to the status quo

This is \*the\* strictly backwards-compatible option: keep the mistake, and just... live with it.

At a cursory glance, this is the simplest option, since it requires no effort, beyond maybe documenting the mistake.

{% admonition(kind="info",title="Side note") %}
If you _do_ choose this option, please please _please_ spare the effort to **document** the bug in a relatively visible way. It is not a great feeling to discover that not only have you "been astonished", but it feels even worse to learn that other people have been through that same "journey" before you.
{% end %}

This option's positive effect is obvious, too: what used to work, keeps working.
That might seem inconsequential, but it has more implications than meets the eye: simply ask the question "why does old stuff matter?"

### Bitrot

[Bitrot][bitrot] is the phenomenon of Things ceasing to Work over time[^rot].
Since adapting code demands effort, bitrot creates churn.
And not just to modify the code itself: those changes need testing, and updating all other related things (documentation, whatever may be depending on or otherwise interacting with what was modified&mdash;yes, bitrot cascades...), _and_ deploying the new modifications.

It thus makes sense that since a backwards-incompatible change creates bitrot, keeping backwards compatibility avoids creating churn for other people.

In addition, the adaptation's changes inherently carry some of the famous "risk" you have probably, if you work in this industry, heard from the mouth of many managers.
But for the sake of clarity, I'll explain what I mean: "risk" is, roughly, "risk that something else will break due to these changes".

Worse still: projects under active development may be able to pay the costs of adapting to the bitrot, but projects that are either abandoned or can't pay those costs... simply won't.
And will thus stick to an old version of RGBDS, despite the disadvantages of _that_.

<figure>

[![XKCD #2224](https://imgs.xkcd.com/comics/software_updates.png "Everything is a cloud application; the ping times just vary a lot.")](https://xkcd.com/2224)

</figure>

### Preservation (of history)

Another aspect worth talking about is preservation.
Arguably, it's only it's a consequence of bitrot, but I feel like it's worth highlighting.

Game Boy development is one of several fields which enjoys not just a flourishing activity today, but also a long and storied past.
This motivates some people to archive said history, document it, and yet more...
But, backwards incompatible changes are a hurdle for those archivists.
Why is that?

Well, let's consider [Pocket Music], which is one of the commercial GB games that was made with RGBDS, and whose source code has recently been published.
The source code is not all that useful on its own: if nothing can be done with it, it's "inert" in a way, and a bit pointless.
So ideally, people would be simply able to get that source code, a copy of RGBDS, and build the code[^tools].

Except... there are a bunch of wrinkles here.

If the version of RGBDS is too old, then it will be missing features and/or bugfixes required to (correctly) build the game.
I think this is fair, because arguing against this implies never adding any features _period_, which I think everyone will agree is unreasonable.

So then, there is a minimum boundary on the version of RGBDS required.
Well, fair enough: any recent version will do, then!

![Image macro whose punchline is “Backwards compatibility breakages:” “Allow us to introduce ourselves,”](breakage.png)

See, if the code relies on any behaviour which was later changed incompatibly, then any version after that breakage is _not_ suitable!
Which means that there now is an _upper_ bound on the required version as well.

This creates logistics problems: if you want to work on more than one single project, you now need more than one version of RGBDS on your machine.
Which now requires you to specify which version of RGBDS, among those on your machine, to use to build this or that project...
And, unless you're fine with having a bunch of identical copies of RGBDS scattered throughout your machine, also means that you need a centralised "RGBDS version manager"[^nvm], since traditional software distribution mechanisms focus on having a single version available globally.

### Knowledge fragmentation

Like preservation, this too is essentially a consequence of bitrot, but with enough weight to merit being discussed separately.

You see, it's not just code that gets affected by bitrot.
"Peripheral" writings, such as documentation, tutorials, and references, are also tightly coupled to the code they relate to.

This means that those writings lose their value with breaking changes; but importantly, _not because of any change to the writing itself_.
This is important, because there is a general assumption that if something doesn't change, then its properties don't either.
For example, reputation, and relatedly, search engine rankings.

The net effect is that prominent knowledge bases become outdated **without any visible signs** of that decay.
This tends to create not only fragmentation of knowledge (again: search results becoming stale without the search engines being aware), but also confusion for people—mostly newbies—who aren't expecting such fragmentation or decay.

[^rot]: The use of the term "rot" can be a little misleading, because code does not intrinsically _rot_; rather, the environment around it changes, eventually rendering it non-functional.

[^tools]: Sure, some extra tools may be necessary, not just RGBDS. But let's focus on just RGBDS for now; the points being brought up also apply to said other tools. (Which also means that the effect compounds, thereby reinforcing the point that back-compat is important!)

[^nvm]: People familiar with the Node ecosystem may have noticed the suspiciously similar naming to [NVM], the Node Version Manager. This may or may not be foreshadowing.

## Fixing the Fuckups

So, those were a lot of good arguments; there is definitely a strong incentive not to break anything... but then, why would we want incur so many negative consequences?
Well, it turns out that inaction itself also has costs.

### Sacrificing the future for the sake of the past

I think this could be described as a sort of "reverse [bitrot]".

As mentioned [in the previous post](@/blog/backcompat/part1.md#the-case-against-back-compat), sometimes backwards-incompatible changes are **necessary** to fix some earlier mistakes.
This means that refusing backwards-incompatible changes also implies _not_ fixing those mistakes.

This decision means that every future project now has to deal with, maybe work around, that mistake.
In a way, the churn that fighting "bitrot" entails, is instead incurred on any _new_ code&mdash;hence my calling this phenomenon "reverse bitrot".

In particular, if people have several options, papercuts will weigh negatively against any option that has them.
Staunchly sticking to inconvenient behaviours means that alternatives will be more compelling to pick than yours.
(And if they don't exist yet, the creation of said alternatives will become more compelling too.)

This, I believe, **dooms the tool to eventual irrelevance**.
...which, when you think about it, sounds awfully similar to the knowledge fragmentation that was touched on earlier.
The difference being that the tool itself becomes obsolete, not just the code using it or the tutorials documenting it.

### You Can't Fight Fate

If the only way to do something is through some undocumented bug or method, people won't stop doing it; they'll just do it the undocumented way.
The corollary is, if your tool can do <var>X</var> and people want to do <var>X</var>, you better give them a way to do <var>X</var>!
Otherwise, they'll find a way to do it anyway, and you'll be stuck supporting that.

The need for change can simply come not from a mistake, so to speak, but from the context around the tool chainging, new use cases arising...
I'd say that users nowadays expect more out of their tools (better UX, more helpful error messages...), and that this need is valid.
Resisting this need is likewise going to doom the tool to irrelevance, being considered as a relic of the past more than something still useful.

Note, however, that this does _not_ mean that tools ought to change regularly!
The old <q>don't fix what ain't broke</q> saying _does_ have value, even though it tends to be abused as an excuse not to fix something that's not _too_ broken yet.

<figure>

[![Picture of Pachatte](pachatte_thumbnail.jpg)](pachatte_full.jpg)

<figcaption>This was a lot of words, so here's a picture of my cat Pachatte. Cat nerds can click the kitty to see her full-res :)</figcaption></figure>

## Picking your poison

The takeaway, to me, is that both options—keeping or breaking backwards compat—end up **causing the same problems**, just in different ways!
Put another way: the costs are the same, they're only paid by different people.

There are biases, though!
For example, existing users will be more vocal and know better how to make themselves heard than new users, particularly if the latter are simply driven away before their first interaction.
Also, I believe it's generally easier to under-estimate the impact (dare I say, the ["astonishment"](@/blog/backcompat/part1.md#it-sounded-better-in-my-head)) on future users compared to existing ones.

Finally, there are subjective[^librul] aspects that preclude any universal answer: perhaps you're not expecting to get many new users, maybe the position of your tool within a toolchain makes breakage unacceptably costly...
For RGBDS, specifically, we have decided that new users are more important than existing ones.
This helps ensure that our tool stays relevant, and both new users _and_ some older ones who've kept up with the changes, have expressed a lot of gratitude about it.

That said, there is also nuance in how to break compat; while I think strict immobility is poor, arguably some ways of breaking backwards compatibility are even worse.
Let's tackle that in the next section!

[^librul]: Possibly there is some merit to the "[software liberal versus conservative]" theory, though I find that post overly biased in favour of conservativeness. This series of posts is, partly, also a response.

[bitrot]: http://gwern.net/holy-war#bitrot
[Pocket Music]: https://web.archive.org/web/20220223141052/https://illusion.64history.net/2022/pocket-music-gbc-source
[NVM]: https://nvm.sh
[beware]: http://bircd.org
[software liberal versus conservative]: http://gist.github.com/mrnugget/49ad3ee4043c746e42187e2820ddc2f6
Binary file added content/blog/backcompat/part2/pachatte_full.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 137 additions & 0 deletions content/blog/backcompat/part3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
+++
title = "Having our 🍰 and 🍽️ eating it too"
authors = [ "stuff@eldred.fr (ISSOtm)" ]

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "Compromising"
+++

We have established that never changing anything leads to buildup of frustration, and that "moving fast and breaking things" is no better.
Let's seek a more reasonable middle ground.

<!-- more -->

## Giving ourselves options

And I mean this literally: giving the user the option to pick the behaviour they want.

The appeal is obvious: if you need compatibility, you can get the old behaviour; if you want improved UX, you can get the new behaviour.
But there are a _lot_ of wrinkles to that.

First off, a simple question.
_What should the default behaviour be?_
Existing users want the old behaviour by default, so that the new version can be dropped in and their code keeps working; new users will want the new behaviour by default, otherwise having to opt into it is, itself, a [papercut].
This leave us exactly at our starting point, with the same tension; the only thing that has been gained is that the tension is _reduced_.
Although, it can also be infuriating to realise that you encountered an obscure error merely because you missed [a single paragraph in the manual][ick-b]... <small>(yes, this example is exaggerated, since it's part of [a pastiche of programming languages][INTERCAL])</small>

The next problem is reasoning about behaviour.
Because this choice necessarily implies that the behaviour of a particular piece also depends on the compatibility configuration.
In particular, this hinders code reuse, since code copy-pasted from any help forum may not work as intended on _your_ setup if it's newer—or worse, it's older!

Another problem is whether it's possible to mix old and new behaviours in a single system; for example, you may have started your own work using more recent features, and eventually want to use a library which turns out to be incompatible with them.

And, lastly, this becomes a combinatorial explosion of old and new code paths, which increases the burden placed onto the maintainers and testers!

{% admonition(kind="tip", title="Prior art") %}

- [CMake] has "policies", which govern individual behaviours, and tend to be hard to keep in mind--they solve the "default behaviour" aspect, but not the behaviour one, and can in fact make it worse.
- [Rust] handles this using its "[editions]", which essentially allow selecting which set of backwards-incompatible changes is desired.
They are scoped at the "crate" (library, roughly) level, which are also guaranteed to continue interoperating across editions (=&nbsp;in a mixed-compat context).
(Although, one would also be right to point out that _some_ breaking changes are made outside of editions...)

{% end %}

## "This store is closing down soon"

Another way to soften the blow is to warn users in advance that the breakage is going to occur.
This can be difficult to implement, depending on the kind of workflow you are normally providing: in more interactive contexts, in particular, it can be difficult to find a way to warn the user without breaking their flow and feeling like an annoyance.
It can be even more difficult to describe what the new way is, too.

The effectiveness and usefulness of such a warning can be improved by providing guidance on how to switch from the old way to the new one (within reason, of course).
Bonus points if some kind of automated tool exists that can perform the transformation automatically&mdash;users will be happy if they don't have to do some kind of mechanical task by hand, particularly if they have a lot of code under their care!

Regardless, this solution has two flaws.

- First, if deprecations are too common, then users will tend to tune them out, and possibly outright disable deprecation warnings (and then complain when the breakage finally occurs).
- Further, how long should the deprecation period last?
One month? Six months? A year? Five?
Or maybe one release? Two? Until the next major version, maybe?

{% admonition(kind="tip", title="Prior art (cont.)") %}

- CMake's policies also serve as deprecation warnings, since relying on the `OLD` behaviour prints a warning[^Wno-dev].
- Rust also sometimes breaks backwards compatibility outside of editions, generally to fix bugs in the compiler.
They do provide generous advance periods ("this will be rejected by a future version of the compiler"), and, interestingly, still mention the deprecation after enforcing it ("this was accepted by a previous version of the compiler")!

{% end %}

[^Wno-dev]: Unless `-Wno-dev` is specified! That's a useful flag for people who are just compiling CMake projects and find its output too verbose 😉

## Semantic Versioning

Well, speaking of version numbers, let's talk about how to convey breakage to users, especially in a more gentle, more ahead-of-time manner.

The question "what goes into a version number?" is as old as software itself, and some folks have decided to formalise a methodology (one of many!) and call it [SemVer] (for "Semantic Versioning").
The gist of SemVer is to split the version into three numbers, and the first one is changed when a backwards-incompatible change is made.
Thus, going from version `2.3.6` to `2.3.11` or `2.5.1` should be safe, but you can expect something to seize if going to version `3.1.0`.

This is useful!
Just looking at two version numbers, you can tell at a glance whether it's safe to upgrade without giving second thought or attention.
In fact, this can even be done by tools, such as Rust's [Cargo] (see, for example, [`cargo update --breaking`]).
But it's not a silver bullet either, because it turns out that what counts as breakage is [not simple], sometimes [astonishing even] (scroll down a bit), and [scarily often subjective]..!

That said, SemVer adds an interesting provision that, essentially, "under `0.x` anything goes".
It was intended to allow "pre-production" testing not just in isolation, but it's often abused because [incrementing the major version number is scary].
This is however achieving little to nothing, since users tend to generally disregard the first number, since it's just a meaningless constant&mdash;I know at least one person who calls RGBDS 0.8.0 "RGBDS 8".
Please consider [the wisdom from SemVer]:

> If you have a stable API on which users have come to depend, you should be 1.0.0.
> If you’re worrying a lot about backward compatibility, you should probably already be 1.0.0.
Unfortunately, this wisdom is ignored [annoyingly often][0ver].

## Version branches 🌳

Another way to ease the pain for users, is to keep maintaining a previous version after making an incompatible change.
This can be considered a compile-time version of the "program options" above: instead of the user making their choice at runtime, they select their options by selecting which version of the probgram they run.

Thus, after releasing 3.0.0, 2.6.2 will still be released; often, incorporating the changes from 3.0.1 or 3.1.0 (a process called "backporting"), perhaps only some of them.

Though, in that last part lies the major downside of version branches: the codebases necessarily diverge.
This means two things.
First, that they need to be developed, tested, etc. separately&mdash;which, like with the "runtime options" above, generates extra effort on your maintainers.
Second, that it may also require adapting the patches during the backporting process.
This can introduce bugs (and thus reinforce the first point), and sometimes be difficult enough not to bother.

## Conclusion

One last note: it is, further, possible to mix and match these techniques: for example, having a few runtime options, deprecating them, and removing them in a later version branch.
See what works for your project!

Anyway; a common thread, perhaps _the_ common thread, between each of these techniques, is that they involve far more work from the maintainer(s), if only to avoid quality slipping.
Some projects, especially the smaller ones, may simply **not have the resources** that would need to be spent on such an endeavour.

_The following paragraph is somewhat personal; please excuse my indulging in a little bit of venting._
Small projects do not have many resources; especially when something is run by a few volunteers, they will usually prioritise what they _enjoy_ working on.
This is by design, since they have no binding obligations to any of their users&mdash;they are, after all, providing the fruit of their labour for free.
Thus, harshly criticising them for their decisions, or otherwise throwing them under the bus, is _not_ helpful.
If anything, it's the opposite, because you'll be either discouraging them, or alienating them (and thus they'll grow to ignore any pertinent or constructive part of what you might be saying).

[papercut]: @/blog/backcompat/part1.md#it-sounded-better-in-my-head
[ick-b]: http://www.catb.org/~esr/intercal/ick.htm#Language_002daffecting-Options
[INTERCAL]: http://www.catb.org/~esr/intercal/
[CMake]: https://cmake.org/cmake/help/v3.31/manual/cmake-policies.7.html#id2
[Rust]: https://rust-lang.org
[editions]: https://doc.rust-lang.org/nightly/edition-guide/
[SemVer]: https://semver.org/
[Cargo]: https://doc.rust-lang.org/stable/cargo/reference/resolver.html
[`cargo update --breaking`]: https://doc.rust-lang.org/stable/cargo/commands/cargo-update.html#option-cargo-update---breaking
[not simple]: https://doc.rust-lang.org/stable/cargo/reference/semver.html
[astonishing even]: https://doc.rust-lang.org/stable/cargo/reference/semver.html?highlight=inference#fn-generalize-compatible
[scarily often subjective]: https://doc.rust-lang.org/stable/cargo/reference/semver.html?highlight=usually#trait-new-default-item
[incrementing the major version number is scary]: https://semver.org/#if-even-the-tiniest-backward-incompatible-changes-to-the-public-api-require-a-major-version-bump-wont-i-end-up-at-version-4200-very-rapidly
[the wisdom from SemVer]: https://semver.org/#how-do-i-know-when-to-release-100
[0ver]: https://0ver.org/
63 changes: 63 additions & 0 deletions content/blog/backcompat/part4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
+++
title = "RGBDS' stance"
authors = [ "stuff@eldred.fr (ISSOtm)" ]

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "Our position"
+++

After having explored the space of options available to us, now is finally time to talk about the decisions that have been taken for RGBDS.

<!-- more -->

## Compat or changes?

It is readily apparent that RGBDS' maintainers have prioritised changes over backwards compatibility; after all, that's the entire _raison d'être_ of this article (series)!

The first question, then, is _why_.
After all, RGBDS is an old tool, with a lot of established users and a lot of projects that have been relying on it.
However, it is also a tool that is still getting used today, in 2025; and, as someone put it, <q>developing for a 90\'s console shouldn't force you to do it like in the 90\'s</q>.

The first foot put down the slippery slope was "oh, the error messages barely indicate where the error occurred".
This is, on its face, uncontroversial to fix; however, doing so required[^required] making changes to the [object file format], which annoyed some people because they had to rebuild their projects from scratch.

Every other change merely falls somewhere else on the "benefits vs. discontent" scale.

TODO:

- Deprecations
- "Crater" runs
- Old versions

[^required]: If you have already thought of some way the change could have been implemented backwards-compatibly, please hold on to that thought for now, I'll address that further down.

## "You could have done better!"

Some critics point out that the changes could have been done while causing less pain.
For example, the "error location" improvement described above could have avoided causing compatibility headaches if RGBDS was kept able to read older object files.

This is both true, and an option that was considered at the time.
But it was rejected, because the complexity would have been high, yet the benefit was guesstimated[^telemetry] to be low.

I want to emphasize something: all of the people working on RGBDS are **hobbyists**, spending their time on it yet providing the fruit of their efforts **for free**.
When they say that something is "too much effort", that is their right, and complaining about it lies somewhere between being unempathetic and being an asshole; especially because everyone and anyone is not only able to re-create RGBDS on their own in a way that they prefer ("with blackjack and hookers!"), but _all_ of the source code is _right there_, so you don't even have to do it from scratch!

And since numbers always catch people's eyes, I would like to point out that someone has ballparked the work I've put into RGBDS at 250,000€ at standard engineer rates in my country, over a period of five years.
I hope this explains why I tend to get a little jumpy and bitter when my[^our] work gets criticised by people who haven't otherwise lifted a finger to contribute[^contrib].

[^telemetry]: It's worth putting emphasis on that "guesstimated" part: we have no usage data whatsoever. This means that we can only rely on feedback from the users we happen to hang out with, which is a huge bias we are aware of. The alternative would be to add in telemetry, but the mere act of bringing up the thought has already created a shitstorm, twice; so let's not go there.

[^our]: By this, I'm referring to the work I've put in myself. RGBDS is **not** my sole work: not only do I stand on the shoulders of giants (by which I mean every maintainer and contributor that came before me) regardless of how much I have personally contributed, but I have been especially helped by the wonderful [Rangi42] since 2020!

[^contrib]: Writing code isn't the only way to contribute! Drafting up actual implementation designs (i.e. going beyond saying "it'd be nice if there was a way to do <var>X</var>", dialoguing with the implementers on _how_ <var>X</var> can be done), collecting feedback from places we aren't in, searching prior art, or even writing [peripheral tools][rgbenv] and [scripts][contrib] is a huge help!

[Rangi42]: https://github.com/Rangi42
[rgbenv]: https://github.com/gbdev/rgbenv#readme
[contrib]: https://github.com/gbdev/rgbds/tree/master/contrib

## Conclusion

With all of that said, and the last section especially getting a little personal&mdash;guilty as charged, I needed a little venting&mdash;I want to leave on a more positive note, and provide guidance on how to avoid such difficult situations in the first place.
25 changes: 25 additions & 0 deletions content/blog/backcompat/part5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
+++
title = "Appendix: How to avoid"
authors = [ "stuff@eldred.fr (ISSOtm)" ]

[taxonomies]
tags = [ "gbdev", "back-compat" ]

[extra]
breadcrumb = "Avoiding"
+++

🚧 NOT WRITTEN YET 🚧

<!-- more -->

This article series has been sitting in my drafts directory for two months now; I want to publish the "main course" now, as it holds water on its own, while I work on this appendix.
Thank you for your comprehension!

<!--
## Painting yourself into a corner
Chesterton's fence (https://fs.blog/chestertons-fence/), talking about short-sighted design decisions; as an example, explain the code that led to `:`-in-labels and how what seemed simple ended up creating far more headaches later (macro arg parsing!)
Adding keywords is a compat break!
-->

0 comments on commit c3e9315

Please sign in to comment.