The Scala 3 compatibility story

Picture of Paweł Marks, Senior Scala Developer - Team Lead

Paweł Marks

Senior Scala Developer - Team Lead

10 minutes read

Key takeaways

  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.

Stability

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 Scala 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.

Patch updates

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.

Minor updates

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

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, a concept of source compatibility exists.  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.

Liked the article?

Share it with others!

explore more on

Take the first step to a sustained competitive edge for your business

Let's connect

VirtusLab's work has met the mark several times over, and their latest project is no exception. The team is efficient, hard-working, and trustworthy. Customers can expect a proactive team that drives results.

Stephen Rooke
Stephen RookeDirector of Software Development @ Extreme Reach

VirtusLab's engineers are truly Strapi extensions experts. Their knowledge and expertise in the area of Strapi plugins gave us the opportunity to lift our multi-brand CMS implementation to a different level.

facile logo
Leonardo PoddaEngineering Manager @ Facile.it

VirtusLab has been an incredible partner since the early development of Scala 3, essential to a mature and stable Scala 3 ecosystem.

Martin_Odersky
Martin OderskyHead of Programming Research Group @ EPFL

VirtusLab's strength is its knowledge of the latest trends and technologies for creating UIs and its ability to design complex applications. The VirtusLab team's in-depth knowledge, understanding, and experience of MIS systems have been invaluable to us in developing our product. The team is professional and delivers on time – we greatly appreciated this efficiency when working with them.

Michael_Grant
Michael GrantDirector of Development @ Cyber Sec Company