The Scala 3 compatibility story
No programming language can succeed long-term without strong stability guarantees. Scala 3 takes this seriously, giving you excellent backward output compatibility and ensuring your code will work in future versions without changes.
1. Scala 3 follows Semantic Versioning.
2. Scala 3 guarantees backward output compatibility forever. A library compiled with Scala 3.0 will still be working with Scala 3.14 in 2033.
3. Scala 3 has an extensive safety net to assure that code that is working today should work in the same way in future releases.
4. Scala adopted a release model very similar to Java with LTS releases that will be ordinary minor versions receiving patches for a long time.
No programming language, no matter how useful and convenient, can succeed long-term without strong stability guarantees. With a stable programming language, developers can confidently expect it to function predictably and consistently over time and update without needing to make any changes in their codebases. This allows developers to focus on building high-quality software that meets users’ needs.
Scala 2’s versioning scheme and compatibility guarantees were a bit peculiar for two reasons:
- It did not follow semantic versioning. Indeed the first number (which was always 2) denoted the basic compiler codebase, and the second number denoted a breaking release. So 2.12 and 2.13 were binary incompatible.
- Follow-up versions such as 2.12 and 2.13 were neither forward nor backward-compatible. A component compiled for 2.12 had to be re-compiled for 2.13. If a component worked for both versions, it would have to be cross-compiled. This posed a significant burden on the Sala ecosystem, where library maintainers had to put in additional work to keep their libraries usable across multiple Scala versions.
Semantic Versioning of the Language
One of the goals of Scala 3 was to solve those problems once and for all. As a sign of firm commitment to stability, the compiler team adopted Semantic Versioning. What does this mean in practice?
First, this means that the version number has a well-defined meaning. It follows the `major.minor.patch` scheme, with each part representing some compatibility guarantee.
Those are updates like 3.0.1 -> 3.0.2 or 3.2.0 -> 3.2.1. They only increment the `patch` version. All changes in patch updates are either usability enhancements (such as better error messages), bugfixes, or are completely internal (refactorings, optimizations).
Those changes are forward and backward-compatible. For example, a library compiled with 3.2.2 can be consumed by a project using Scala 3.2.0 and vice-versa.
Updates like 3.0.3 -> 3.1.0 or 3.2.2 -> 3.3.0 are called minor updates. Incrementing the `minor` version usually means that there were new definitions added to the standard library. Those updates can also add a new backward-compatible feature. Those features are bigger usability improvements (like adding linting in 3.3.0) or loosening some implementation restrictions (like allowing exports in extension clauses in 3.2.0). Those changes are never intended to change the semantics of the working code.
The changes in minor releases are always backward compatible. For instance, code compiled with Scala 3.1 can still be used in Scala 3.2, 3.3, 3.4… or any future version forever. However, the opposite is not true. Code compiled with Scala 3.2 cannot be used in a Scala 3.1 project. Allowing this would not be safe. For instance, the 3.2 code might want to access a library method that did not exist in 3.1.
Major updates are the only kind that can introduce backward-incompatible changes. To make such changes, we would need to release Scala 4. We are currently not working on Scala 4, and there are no plans to start any work on it in the foreseeable future. Scala 3 will keep its backward compatibility forever.
More on output compatibility
The guarantees described above are called output compatibility. They encompass binary compatibility (compatibility on the level of generated bytecode) and TASTy compatibility (the possibility for the newer versions to read well-defined and structured metadata describing the original source code necessary for correct linking).
Some languages, like Rust, require users to compile the sources of all their dependencies. Scala is different. Developers can get already compiled artifacts from a repository like Maven Central. Thanks to the output compatibility guarantee, a library published there can be used by projects compiled with any future version of Scala, without the need for cross-publishing or any other intervention from the maintainers.
This also works nicely when a critical security problem is found in some older but still used version of the library that was compiled with a not-up-to-date version of the language. The maintainers can fix the bug easily without needing to bump the compiler version and then release the fix in a patch release. All projects that depend on a problematic version of the library can switch to the newly-released patch, no matter what version of the language they are using.
The source compatibility safety net
Apart from output compatibility, there exists a concept of source compatibility. It means that a developer can change the version of the compiler they are using without making any changes in the source code and still receive the same resulting program. As with output compatibility, we (slightly counterintuitively) can say that two versions of the compiler are source backward compatible if the code created for an older version works with a newer version. If the change is the reverse, and the developer is downgrading the compiler, we call it forward compatibility.
In the compiler team, we are paying attention to source compatibility and ensuring that code that is compiling today should compile with the future versions of the compiler. We cannot always guarantee that. Like any other compiler and any other piece of software, the Scala compiler can have bugs. In very rare cases, the fact that some code is considered correct may be a result of a bug in the compiler. Fixing this bug may result in code that was previously compiling, stopping doing so in newer versions. Moreover, sometimes fixing the compiler bug affecting one snippet of code may slightly change the type inference in another snippet, causing problems, like failures related to implicit search.
Does that mean that you cannot rely on the stability of the compiler, and you should expect breaking changes? Absolutely not! There is a multilayer safety net to catch source incompatibilities early so they do not make it into the stable versions of the compiler.
The first layer of such a net is an extensive set of compiler tests. Currently, it contains over 12 thousand Scala files. Every time a new bug is found, at least one new snippet is added. Failure of any of those snippets on any of the pull requests means that the PR cannot be merged.
The second layer is a compilation of fixed versions of over 70 popular Scala libraries. Failures of compilation or tests in any of those libraries also block the merging of the PR.
The last layer is the Open Community Build, introduced around the release of Scala 3.2. It runs weekly, building the entire Scala 3 open-source ecosystem. It tries to build every single open-source project ever released for Scala 3. Every failure here is investigated and treated as a high-priority bug. The post linked above describes an interesting example of finding and fixing such a bug.
Additionally, we run it for every RC and sometimes for bigger PRs. We treat regressions detected by the Open CB seriously. Many times we have prolonged the RC period and delayed releases because of small regressions found this way. For us, a stable release will always win over a fast release.
As you can see, while it is not impossible to break the source compatibility by accident, right now, we have an incredibly strong set of tools to prevent that.
LTS and Scala Next
Stability is of paramount value for the industry. This is why we decided that we could go even further than committing to Semantic Versioning. We are introducing Long Term Support Versions of the compiler. Those selected minor versions are guaranteed to receive patch updates containing bug fixes, usability enhancements, and optimizations for a period of at least three years, possibly longer. As those will be within a single minor version, they are guaranteed to be fully forward and backward, source and output compatible. They will also maintain all of our other guarantees. They will be able to consume libraries compiled with older versions of Scala 3 (including non-LTS ones) and accept sources created for previous releases.
The other minor releases (equivalent to Java’s feature releases) will be codenamed Scala Next to distinguish them from LTS releases. This doesn’t change their guarantees. They will still maintain backward output and source compatibility. The only difference is that they will receive patch updates only until the next minor release.
The upcoming Scala 3.3.x series will be the first LTS release.