My thoughts on breakage, and I'll be in DConf Online 2020

Posted 2020-10-05

Gonna write just a little this time to catch back up: some thoughts on semver and breakage.

BTW I was officially notified of my acceptance into DConf Online in November (indeed, the tentative schedule puts it on my birthday! lol). I'm still working out the exact details, but I'll be livecoding something, perhaps a little online multiplayer game.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

My thoughts on breaking changes and versioning

You're probably familiar with the semver idea: three numbers, x.y.z.

z is changed with a "backwards compatible bug fix", an "internal change that fixes incorrect behavior". This is the patch version.

y is changed if "new, backwards compatible functionality is introduced to the public API" or if anything is marked deprecated. This is the minor version.

x is changed if "backwards incompatible changes are introduced to the public API." This is the major version.

Those quotations are from the official website. But what do they really mean, and how does it apply to D? Let's explore what "backwards compatible" means.

In my view, there's four major categories of breakage: build process breakage, compiler-error breakage, architecture breakage, and runtime breakage.

Build Process Breakage

Build process breakage is when you change something in the library that affects the user's compilation arguments. Their code stays the same, but their makefile (or whatever) might have to change. Some people don't even consider this to matter, since they see building the library as an internal matter. But, in my code, I guarantee that you can just grab a file and drop it in in many cases. So that promise is broken if I change the build by adding a dependent module or whatever.

Nevertheless, build process breakage may not apply to you, and if it does, you can usually publish a migration path for the users easily enough. But, does this merit a major version number bump? For me, yes. For you, it might just be a patch.

Compiler error breakage

This is when the build process issues compile errors that the user must address in their code. Could be as simple as a function renaming to something more involved like an argument type change.

The semver website says deprecations are minor version changes. So I suppose warnings would be minor, but compile errors are major changes?

It says adding a function in a backward compatible way is minor, but the truth is ANY addition to the public api has the potential of issuing a compile error. Suppose you add a function mylib.write. If the user had also imported std.file, their code will now get an error about an ambiguous name. Easy to fix, but an error nonetheless.

So what, exactly is actually backward compatible? It seems to me just about anything has the potential of breaking things since compile errors can be triggered by any number of innocent modifications.

Nevertheless, I think as a practical matter, additions in D are probably best signified as minor versions, whereas removals are major versions. Just... I'm not sure this actually solves the problem semver is trying to solve.

Architecture breakages

This is when the user must significantly modify their code to keep it working. For example, before your API was synchronous, and now it is based on callback functions. That's certainly a backward incompatible change! But is that really in the same category of, say, a function being renamed? You can fix those pretty mindlessly just from compile errors, but this means rethinking your program.

I kinda feel this needs something beyond a major version bump. You could argue perhaps it should be forked into a whole new library name.

Runtime breakage

Finally, of course, we have code that changes at runtime. Perhaps it issues an error it wouldn't have done before. Perhaps it used to say 2+2 = 5 and now you fixed it to say 2+2 = 4. In your eyes, that's a bug fix, so it gets a patch version bump... but to some user, they might have accidentally relied on that, and now, say, their digital signature checking code randomly fails and they have no idea why.

These would probably generally fall into major version bumps, but one user's bug fix is another user's breakage so... it is kinda ambiguous too.

Of course, in D, we would try to avoid these whenever we can by making them compile errors instead.

Conclusion

idk. I consider any change that might require user code fixes (aside from esoteric name conflicts, but I do try to keep my names fairly unique to avoid this) in the arsd repo to be major changes. And since it is the whole assorted repo that dub versions, I bump the whole thing's number fairly frequently. As such, I'm among the largest version number libs on the dub website, though I betcha I actually have fewer overall breaking changes!

Otherwise, I tend to add the minor version for each supported addition and reserve the patch number for the cases where I accidentally pushed something broken and need to go back to fix it.

But... I still think the best thing to do is to just try not to break at all. If you do, make a new module so old and new can live side by side. semver's promise of user choice is just not something I would want to rely on due to the ambiguities in interpretation and general difficulty in actually using two major versions side-by-side in the same program.