StableMIR is being developed to become the public interface of the Rust compiler to analysis tools that can be developed outside of the Rust main repository.
It is intended to be more stable than the internal APIs, and to follow semantic versioning. For that, the goal is to start publishing a stable_mir
crate on crates.io, which can be explicitly selected by tool developers.
This document proposes what the first releases will look like, as well as how development will be done in the Rust compiler in between version releases.
In our first development phase of StableMIR, we focused on adding enough coverage for static analyzers tools to use in order to interpret a Rust program.
For that, we added two crates to the Rust compiler, stable_mir
and rustc_smir
, the first is a shallow shell that implements the public APIs while the second implements the interface between public APIs and the compiler internal APIs, including translation. Because of that, the rustc_smir
crate depends on the stable_mir
crate.
This dependency order makes it harder for the user to select which stable_mir
crate to depend on, since the supported version would have to be hard coded in the compiler's rustc_smir
crate.
Our goal is to publish stable_mir
in crates.io, and have the Rust compiler rustc_smir
to implement an interface known to stable_mir
to provide the communication with rustc. We would also like to reduce friction for rustc developers.
We would like to propose a change to the crates architecture and invert the dependency between stable_mir
and rustc_smir
. I.e., the stable_mir
crate should depend on the rustc_smir
crate, not the other way around. The main advantages are:
stable_mir
crate to be compatible with different compiler versions, by using conditional compilation that checks the compiler nightly version.rustc_smir
would be completely agnostic of stable_mir
, which would allow different stable_mir
versions to interface with the same compiler version.stable_mir
crate hosted in the compiler can be kept up-to-date with changes toFor that to happen, we will need to make the rustc_smir
interface based only on internal APIs. The rustc_smir
would have an implementation of the CompilerInterface
similar to the one existing today, i.e., the same functions and logic, however, their input and output would all be internal rustc constructs. There would be no CompilerInterface
trait.
We would then move the translation between internal rustc and stable constructs to a private module[1] inside the stable_mir
crate. This translation would incorporate conditional compilation for handling divergence between rustc versions. The facade of this internal module would be a proxy, CompilerInterface
, which would have the same set of functions implemented in the rustc_smir
crate, however, it would be responsible for the translation between stable / internal constructs.
See "Examples" for more details on how this would work.
We will need to keep two stable_mir
crates:
stable_mir
crate in the compiler, which will be kept up-to-date with the compiler, and it will serve as the basis for the next major release of stable_mir
. This stable_mir
has no compatibility or stability guarantees.Whenever a change is made to the Rust compiler that breaks the current published version of stable_mir
, the StableMIR group will have to publish a new version (minor or major) that is compatible with the new compiler:
If the change can be easily addressed by adding some conditional compilation, the team should prefer doing that.
The stable_mir
crate will have a build.rs
file that checks the compiler nightly version (which is the date it was published). Depending on the date, we will set a cfg nightly_features
which will be used inside the stable_mir
to check for features availability.
The stable_mir
crate now needs to be implemented for versions with and without the breaking change.
See "Changes to Variants" for an example.
In case of a significant change, or there's too many nightly_features
being tracked, the StableMIR group may choose to deprecate the current major version and release a new one.
In order to release a new major version, we will copy the compiler's stable_mir
crate into the project stable_mir
. We will create a release note documenting the major changes, and publish it to crates.io.
New major versions may only be compatible with newer compiler versions. A compilation error should be triggered if an unsupported version is used suggesting users to upgrade to the minimum required version.
In order to deprecate a major version, we recommend creating a minor version that will trigger a compilation error for unsupported versions of the compiler. This error should suggest users to migrate to a newer version of stable_mir
.
Note that tools could still be developed on the top of the compiler version of the crate, but they would have to face constant breakage. We still expect rustc developers to keep the compiler crate (2). They won't however need to worry about breaking change, delaying renames or things like that.
The basic user flow for retrieving the kind of a type today is implemented as follow (some details were omitted for simplicity):
I.e.: The conversion between stable_mir
and MIR
components is done inside the Rust compiler. Thus, the compiler must be aware of the stable_mir
version. The implementation is the following:
With the new proposal, the conversion will now live inside stable_mir
crate, and the rustc_smir
crate inside the compiler will not be aware of it.
For the same use case as above, this will be the new sequence flow to retrieve the kind of a type.
Note that we would be basically splitting the current impl Context
into 2, the stable_mir::CompilerInterface
and the rustc_smir::CompilerContext
. The one living inside rustc_smir
would be able to query the Rust compiler and do any information processing. While the proxy living inside stable_mir
would only be responsible for translating internal to stable constructs, as well as caching any result, such as the def_id
map.
Here is how it would be implemented:
In the case where an internal compiler function, such as Ty::kind
, is renamed, changes would only need to occur in the rustc_smir
module. Like today, that would not affect the end user or the stable_mir
crate.
Changes would need to be in the conversion inside covert.ty
, and we would bump the stable_mir
crate minor version to support newer versions of the compiler.
Let's say a new TyKind
was added: TyKind::Dummy
, we could add a function try_ty_kind()
to the existing version of StableMIR:
The conversion would now become:
And the crate build.rs
would add to nightly_feature
the "DummyTyKind" based on the nightly compiler version.
Users that would like to use new versions of the compiler would just need to update the minor version they are using. Users of older compiler versions would still be able to update the minor version.
Adding a new StableMIR functionality would not break older versions of StableMIR or users using older compiler versions.
If changes are needed on the compiler side, such as adding a new method to CompilerContext
, the new feature should be guarded using the nightly_feature
mechanism mentioned above. In this case, the new feature will only be available for users of the new compiler.
This proposal will allow the published stable_mir
crate to be compatible with different versions of the Rust compiler. It will also reduce the current friction with rustc developers, since they would no longer need to worry about backward compatibility.
The main downsides of the proposed solution are the upfront cost of having to refactor our existing crates, and the extra maintenance to keep the current major version compatible with newer versions of the compiler. This maintenance cost will be taken by the StableMIR group.
However, the StableMIR group will always have the option to publish a new major version using the compiler's stable_mir
crate. This can be done whenever backward compatibility due to newer changes in the compiler are too costly.
I.e.: we would be able to keep a better support to multiple versions of the compiler when possible, while keeping the basis to a new major version in place.
See the appendix for alternatives considered so far.
Here are a few alternatives that we have explored so far.
The initial version proposed. The compiler would still have 2 crates, stable_mir
and rustc_smir
. The stable_mir
crate would represent the latest unreleased version of stable_mir
, while rustc_smir
would be aware of different stable_mir
.
This approach is described here: https://hackmd.io/XhnYHKKuR6-LChhobvlT-g#MVP-Design
The main drawback of this approach would be the overhead to Rust compiler developers, since they would need to be aware of all stable_mir
versions that are supported by the compiler at the time they are making changes to rustc.
Described in more details here: https://hackmd.io/WXdHKkVAQMaEdk4xLxv8Tg
In this alternative they end up with at least two versions of the stable_mir
crate, where the one that has been published to crates.io
has to be able to translate from stable_mir
included in the compiler. The more stable_mir
crates are published, the number of translation layers would increase.
Updates to the stable_mir
crate would always need to be made together with a compiler upgrade, which would force us to constantly release new major versions of stable_mir
, even if there was no change to the public APIs. I.e., without bumping the major version, users running cargo update
could end up with a version of stable_mir
that is not compatible with the current compiler.
The compiler tracks a single StableMIR version like option 2. However, we won't support any proxy.
The build script of stable_mir
will use this option to validate whether the current version is supported, and if not, it will error telling users to upgrade to the supported version.
Not every rust compiler would support a published StableMIR.
This alternative would have minimum impact on the rustc development, however, it would greatly impact the stable_mir
usability.
We would still expose internal
and stable
methods as unstable methods for helping users bypass stable-mir
. ↩︎