unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
rust-lang/rust
senior8/10
type-systemserror-handlingcompiler-internals

Error on projection of dyn noncompat type in old trait solver

Project: rust-lang/rust | Language: Rust | Concepts: type-systems, error-handling, compiler-internals | Difficulty: senior
Link: https://github.com/rust-lang/rust/pull/154992

The problem

The Rust compiler's old trait solver allowed projections like <dyn Trait>::AssocType even when Trait was not "dyn-compatible" (i.e., not object-safe). This led to either confusing errors or internal compiler errors (ICEs) when users tried to project associated types from trait objects of non-dyn-compatible traits.

Why it's non-trivial

A naive fix might just block such projections everywhere, but:

  • The check must be done in the right phase of trait solving to avoid false positives/negatives.
  • Some situations (like canonical queries or certain internal checks) must not immediately error, but instead delay or suppress the error reporting.
  • The fix must avoid introducing spurious errors or ICEs in edge cases, and must not break code that is valid or intended to compile.

The approach

1. Early error for non-dyn-compatible trait object projections

The core fix adds a check in the projection logic to detect when a projection is being made from a dyn Trait where Trait is not dyn-compatible. If so, it reports an error or delays a bug as appropriate:

+    let self_ty = selcx.infcx.shallow_resolve(obligation.predicate.self_ty());
+    if let ty::Dynamic(data, ..) = self_ty.kind() {
+        if let Some(def_id) = data.principal_def_id() {
+            let tcx = selcx.tcx();
+            // Delaying a bug is fine here:
+            // - In case of a dymmy span, we are in a canonical query.
+            // - `CheckAssociatedTypeBounds` means that the trait object is part of the trait's item bounds
+            //   or the impl's associated type; both are checked at their own definition
+            //   where the error is already reported with a better span.
+            if !tcx.is_dyn_compatible(def_id) {
+                let span = obligation.cause.span;
+                let guar = if span.is_dummy()
+                    || matches!(
+                        obligation.cause.code(),
+                        ObligationCauseCode::CheckAssociatedTypeBounds { .. }
+                    ) {
+                    tcx.dcx().span_delayed_bug(
+                        span,
+                        format!(
+                            "projection from non-dyn-compatible trait `{}`",
+                            tcx.def_path_str(def_id)
+                        ),
+                    )
+                } else {
+                    let violations = tcx.dyn_compatibility_violations(def_id);
+                    report_dyn_incompatibility(tcx, span, None, def_id, &violations).emit()
+                };
+                return Ok(Projected::Progress(Progress::error_for_term(
+                    tcx,
+                    obligation.predicate,
+                    guar,
+                )));
+            }
+        }
+    }

What this does:

  • Resolves the self type of the projection obligation.
  • If it's a dyn Trait, checks if Trait is dyn-compatible.
  • If not, emits an error at the projection site (with context-sensitive handling for internal/canonical queries).
  • Returns an error result to halt further projection.

2. Prevent candidate assembly for non-dyn-compatible trait objects

The candidate assembly logic for trait object projections is also updated to avoid assembling candidates for non-dyn-compatible traits:

+    // Projecting `dyn Trait` is only valid when `Trait` is dyn-compatible.
+    if data.principal_def_id().is_some_and(|def_id| !tcx.is_dyn_compatible(def_id)) {
+        return;
+    }

What this does:

  • Early-returns from candidate assembly if the trait object is not dyn-compatible, preventing further (invalid) projection attempts.

3. Test updates and new tests

  • Adjusts existing tests to expect fewer errors (since only the relevant error is now emitted).
  • Adds a new test (dyn-proj-non-dyn-compat.rs) to check that projecting from a non-dyn-compatible trait object produces a clear and concise error.

Example from the new test:

+fn make() -> impl Trait<Output = <dyn Trait<Output = &'static str> as Trait>::Output> {
+    //~^ ERROR the trait `Trait` is not dyn compatible
+    MyImpl
+}

What this does:

  • Asserts that the compiler now emits a single, clear error when projecting from a non-dyn-compatible trait object.

From the review thread

No notable review discussion on this PR.

Takeaways

  • Validate trait object compatibility before performing projections.
    Always check if a trait is dyn-compatible before allowing projections from dyn Trait to prevent confusing errors or ICEs.

  • Emit context-sensitive diagnostics.
    When reporting errors, consider the context (e.g., canonical queries vs user code) to avoid duplicate or misleading error messages.

  • Guard candidate assembly with invariants.
    Preventing candidate assembly for invalid cases (like non-dyn-compatible traits) avoids unnecessary computation and downstream errors.