Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,25 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
submodules: recursive

- name: 'Install yq'
uses: mikefarah/yq-action@v4

- name: 'Read rustc commit from rust-toolchain.toml'
id: rustc-meta
run: |
set -euo pipefail
COMMIT=$(yq -r '.metadata.rustc-commit' rust-toolchain.toml)
if [ -z "$COMMIT" ] || [ "$COMMIT" = "null" ]; then
echo "::error::metadata.rustc-commit not found in rust-toolchain.toml"
exit 1
fi
echo "rustc-commit=$COMMIT" >> "$GITHUB_OUTPUT"
- name: 'Check out Rust repo'
uses: actions/checkout@v4
with:
repository: rust-lang/rust
ref: a2545fd6fc66b4323f555223a860c451885d1d2b # hash of Hardcoded Rust version
ref: ${{ steps.rustc-meta.outputs.rustc-commit }}
path: rust
fetch-depth: 1

Expand Down
52 changes: 52 additions & 0 deletions docs/adr/003-compat-layer-for-rustc-internals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ADR-003: Compatibility layer for rustc internal APIs

**Status:** Accepted
**Date:** 2026-02-21

## Context

stable-mir-json hooks into rustc's internal APIs (`rustc_middle`, `rustc_smir`, `rustc_span`, etc.) to extract MIR data. These APIs are unstable; they change regularly across nightly releases, and the crate names themselves get renamed (the `stable_mir` crate became `rustc_public`, `rustc_smir` became `rustc_public_bridge`, etc.). Before this decision, rustc internals were used directly throughout the codebase: `printer.rs`, `mk_graph/`, `driver.rs`, and various helpers all had their own `extern crate` declarations and direct imports. So a toolchain bump meant hunting through every file that touched a changed API; not fun, and easy to miss things.

## Decision

Route all rustc internal API usage through a single `src/compat/` module. The module re-exports crate names (so a rename like `stable_mir` to `rustc_public` is a one-line alias change in `compat/mod.rs`) and wraps unstable functions behind stable signatures (so a changed calling convention is absorbed in one place).

The compat layer does *not* try to abstract over stable MIR's own public API. When `stable_mir` (the public, downstream-facing API) changes its types, any consumer has to adapt; that's by design. The boundary is: if it's a rustc implementation detail, it goes through compat; if it's the stable MIR contract, it flows through directly.

`src/driver.rs` is the one exception; it uses `rustc_driver` and `rustc_interface` directly because it *is* the rustc integration point. Everything else goes through compat.

## Consequences

**What the compat layer absorbs (rustc internals):**

| Change | Absorbed in |
|--------|-------------|
| `collect_and_partition_mono_items` API changes | `compat/mono_collect.rs` |
| `RunCompiler::new().run()` becoming `run_compiler()` | `driver.rs` |
| `stable_mir` renamed to `rustc_public` | `compat/mod.rs` (re-exported as alias) |
| `rustc_smir` renamed to `rustc_public_bridge` | `compat/mod.rs`, `driver.rs` |
| `IndexedVal` trait moving between crates | `compat/mod.rs` (re-exported) |
| `FileNameDisplayPreference` variants changing | `compat/spans.rs` |

None of these changes leaked into `printer.rs` or `mk_graph/`. The abstraction worked as designed.

**What still propagates (stable MIR public API evolution):**

- `Rvalue::AddressOf` changed from `Mutability` to `RawPtrKind`
- `StatementKind::Deinit` and `Rvalue::NullaryOp` removed
- `AggregateKind::CoroutineClosure` added
- `Coroutine` and `Dynamic` field count changes
- `Ty::visit()` return type changed from `()` to `ControlFlow<T>`

These affect `printer.rs` and `mk_graph/` regardless of the compat layer. Any consumer of stable MIR would need to handle them; there's nothing we can (or should) do about that.

**The mk_graph gap (now fixed).** Turns out the `mk_graph/` files originally declared their own `extern crate stable_mir`, bypassing the abstraction entirely. This was introduced in commit `e9395d9` (PR #111) before the compat layer existed; it wasn't an oversight so much as a timing issue. The 13-month toolchain bump exposed the cost: when `stable_mir` was renamed to `rustc_public`, all 5 mk_graph files needed updating, while `printer.rs` needed zero import changes because it already went through compat. Commit `307dcb8` closed this gap by routing all mk_graph imports through `use crate::compat::stable_mir`.

## Validation

We stress-tested the abstraction against two toolchain bumps to see if it actually holds up in practice:

- **6-month jump** (nightly-2024-11-29 to nightly-2025-06-01, rustc 1.85 to 1.89): all internal API changes contained in `compat/` and `driver.rs`
- **13-month jump** (nightly-2024-11-29 to nightly-2026-01-15, rustc 1.85 to 1.94): same containment, plus the major `stable_mir` to `rustc_public` crate rename absorbed by a single alias in `compat/mod.rs`

Both branches compile and are available for reference: `spike/toolchain-2025-06` and `spike/toolchain-2026-01`, each with a detailed `rustc-<version>.md` breakdown.
6 changes: 6 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[toolchain]
channel = "nightly-2024-11-29"
components = ["llvm-tools", "rustc-dev", "rust-src", "rust-analyzer"]

# Ignored by rustup; used by our test scripts.
# This is the rustc commit that backs the nightly above.
# UI test scripts automatically checkout this commit in RUST_DIR_ROOT.
[metadata]
rustc-commit = "a2545fd6fc66b4323f555223a860c451885d1d2b"
95 changes: 95 additions & 0 deletions src/compat/bridge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Stable<->internal conversions and OpaqueInstanceKind.
//!
//! This module wraps rustc-internal instance kind queries behind an owned,
//! lifetime-free representation so that the rest of the codebase doesn't
//! need to carry `'tcx` lifetimes for link map keys.

use std::hash::{Hash, Hasher};

use super::middle;
use super::rustc_internal;
use super::stable_mir;
use super::TyCtxt;
use stable_mir::mir::mono::Instance;

/// Owned, lifetime-free replacement for `middle::ty::InstanceKind<'tcx>`.
///
/// The actual `InstanceKind` usage is narrow:
/// 1. Serialized as `format!("{:?}", kind)` (a Debug string)
/// 2. Checked via `is_reify_shim()` (a single pattern match)
/// 3. Used for `Hash`/`Eq` in `LinkMapKey` (map keying)
///
/// This struct captures all three via owned data, eliminating the need
/// to propagate the `'tcx` lifetime through `LinkMapKey`, `FnSymInfo`,
/// `SmirJson`, and `SmirJsonDebugInfo`.
#[derive(Clone, Debug)]
pub struct OpaqueInstanceKind {
debug_repr: String,
pub is_reify_shim: bool,
}

impl PartialEq for OpaqueInstanceKind {
fn eq(&self, other: &Self) -> bool {
self.debug_repr == other.debug_repr
}
}

impl Eq for OpaqueInstanceKind {}

impl Hash for OpaqueInstanceKind {
fn hash<H: Hasher>(&self, state: &mut H) {
self.debug_repr.hash(state);
}
}

impl std::fmt::Display for OpaqueInstanceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.debug_repr)
}
}

/// Create a monomorphized Instance from a stable DefId (wraps `Instance::mono`).
pub fn mono_instance(tcx: TyCtxt<'_>, id: stable_mir::DefId) -> Instance {
let internal_id = rustc_internal::internal(tcx, id);
let internal_inst = middle::ty::Instance::mono(tcx, internal_id);
rustc_internal::stable(internal_inst)
}

/// Resolve an unevaluated constant into a (MonoItem, symbol_name) pair.
///
/// This wraps `middle::ty::Instance::try_resolve` and the internal mono item
/// symbol name resolution, keeping those internal APIs out of printer.rs.
pub fn resolve_unevaluated_const(
tcx: TyCtxt<'_>,
def_id: stable_mir::DefId,
args: stable_mir::ty::GenericArgs,
) -> (stable_mir::mir::mono::MonoItem, String) {
use super::middle::ty::TypingEnv;
let internal_def = rustc_internal::internal(tcx, def_id);
let internal_args = rustc_internal::internal(tcx, args);
let maybe_inst = middle::ty::Instance::try_resolve(
tcx,
TypingEnv::post_analysis(tcx, internal_def),
internal_def,
internal_args,
);
let inst = maybe_inst
.ok()
.flatten()
.unwrap_or_else(|| panic!("Failed to resolve mono item for def {:?}", def_id));
let internal_mono_item = middle::mir::mono::MonoItem::Fn(inst);
let item_name = crate::compat::mono_collect::mono_item_name_int(tcx, &internal_mono_item);
(rustc_internal::stable(internal_mono_item), item_name)
}

/// Extract an `OpaqueInstanceKind` from a stable MIR `Instance` by
/// converting to the internal representation and capturing the debug
/// string and reify-shim flag.
pub fn instance_kind(tcx: TyCtxt<'_>, inst: &Instance) -> OpaqueInstanceKind {
let internal_inst = rustc_internal::internal(tcx, inst);
let kind = internal_inst.def;
OpaqueInstanceKind {
debug_repr: format!("{:?}", kind),
is_reify_shim: matches!(kind, middle::ty::InstanceKind::ReifyShim(..)),
}
}
58 changes: 58 additions & 0 deletions src/compat/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Compatibility layer for rustc internal APIs.
//!
//! Every direct `rustc_*` import and raw `TyCtxt` query lives inside this
//! module (or one of its submodules). Code outside `compat` (and `driver.rs`)
//! should never touch rustc internals directly; it should go through the
//! types and functions re-exported here instead.
//!
//! The payoff: when a nightly toolchain upgrade moves or renames an internal
//! API, the fix stays inside `compat/` and nothing else needs to change.
//!
//! # Submodules
//!
//! | Module | Purpose |
//! |--------|---------|
//! | [`bridge`] | Stable-to-internal conversions (`Instance`, `InstanceKind`, unevaluated consts) |
//! | [`mono_collect`] | Monomorphization collection and symbol naming |
//! | [`output`] | Output filename resolution from the compiler session |
//! | [`spans`] | Span-to-source-location resolution |
//! | [`types`] | Type queries: generics, signatures, discriminants, attributes |
//!
//! # Re-exports
//!
//! The crate-level re-exports below give callers access to the handful of
//! rustc types that inevitably appear in public signatures (`TyCtxt`,
//! `DefId`, etc.) without requiring them to know which rustc crate the
//! type actually lives in.

pub extern crate rustc_middle;
pub extern crate rustc_monomorphize;
pub extern crate rustc_session;
pub extern crate rustc_smir;
pub extern crate rustc_span;
pub extern crate stable_mir;

// We use rustc's vendored serde rather than pulling in our own copy.
// Having two serde versions causes version-mismatch errors when
// serializing types that come from the compiler.
pub extern crate serde;
pub extern crate serde_json;

/// Alias for `rustc_middle`; keeps import paths shorter.
pub use rustc_middle as middle;
/// The compiler's typing context; threaded through most compat functions.
pub use rustc_middle::ty::TyCtxt;
/// Bridge between stable MIR types and rustc internals.
pub use rustc_smir::rustc_internal;
/// Convenience re-export: converts a stable MIR value to its internal rustc
/// counterpart.
pub use rustc_smir::rustc_internal::internal;
/// Rustc's definition identifier. Re-exported so callers outside `compat`
/// don't need to depend on `rustc_span` directly.
pub use rustc_span::def_id::DefId;

pub mod bridge;
pub mod mono_collect;
pub mod output;
pub mod spans;
pub mod types;
38 changes: 38 additions & 0 deletions src/compat/mono_collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Mono item collection and symbol naming.
//!
//! Wraps `tcx.collect_and_partition_mono_items()`, `item.symbol_name()`,
//! and the `rustc_internal` stable/internal conversions needed for naming.

use super::middle;
use super::rustc_internal;
use super::stable_mir;
use super::TyCtxt;
use stable_mir::mir::mono::MonoItem;

/// Collect all monomorphized items from the compiler.
pub fn mono_collect(tcx: TyCtxt<'_>) -> Vec<MonoItem> {
let units = tcx.collect_and_partition_mono_items(()).1;
units
.iter()
.flat_map(|unit| {
unit.items_in_deterministic_order(tcx)
.iter()
.map(|(internal_item, _)| rustc_internal::stable(internal_item))
.collect::<Vec<_>>()
})
.collect()
}

/// Get the symbol name for a mono item (the mangled linker name).
pub fn mono_item_name(tcx: TyCtxt<'_>, item: &MonoItem) -> String {
if let MonoItem::GlobalAsm(data) = item {
crate::printer::hash(data).to_string()
} else {
mono_item_name_int(tcx, &rustc_internal::internal(tcx, item))
}
}

/// Get the symbol name for an internal (non-stable) mono item.
pub fn mono_item_name_int<'a>(tcx: TyCtxt<'a>, item: &middle::mir::mono::MonoItem<'a>) -> String {
item.symbol_name(tcx).name.into()
}
24 changes: 24 additions & 0 deletions src/compat/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Output filename resolution.
//!
//! Wraps `tcx.output_filenames().path(OutputType::Mir)` so that callers
//! don't need to import `rustc_session` directly.

use std::path::PathBuf;

use super::rustc_session::config::{OutFileName, OutputType};
use super::TyCtxt;

/// Resolved output destination for MIR-derived files.
pub enum OutputDest {
Stdout,
File(PathBuf),
}

/// Resolve the MIR output path from the compiler session, replacing
/// the extension with the given one.
pub fn mir_output_path(tcx: TyCtxt<'_>, extension: &str) -> OutputDest {
match tcx.output_filenames(()).path(OutputType::Mir) {
OutFileName::Stdout => OutputDest::Stdout,
OutFileName::Real(path) => OutputDest::File(path.with_extension(extension)),
}
}
28 changes: 28 additions & 0 deletions src/compat/spans.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Span-to-source-location resolution.
//!
//! Wraps the `source_map().span_to_location_info()` internal API
//! so that callers don't need to touch `rustc_span` directly.

use super::internal;
use super::rustc_span;
use super::stable_mir;
use super::TyCtxt;
use stable_mir::ty::Span;

/// Source location tuple: `(file, lo_line, lo_col, hi_line, hi_col)`.
pub type SourceData = (String, usize, usize, usize, usize);

/// Resolve a stable MIR span to a (file, lo_line, lo_col, hi_line, hi_col) tuple.
pub fn resolve_span(tcx: TyCtxt<'_>, span: &Span) -> SourceData {
let span_internal = internal(tcx, span);
let (source_file, lo_line, lo_col, hi_line, hi_col) =
tcx.sess.source_map().span_to_location_info(span_internal);
let file_name = match source_file {
Some(sf) => sf
.name
.display(rustc_span::FileNameDisplayPreference::Remapped)
.to_string(),
None => "no-location".to_string(),
};
(file_name, lo_line, lo_col, hi_line, hi_col)
}
Loading