Skip to content

ICE: unwrap() on None in orphan check when UncoveredTyParamCollector finds no param_def_id #155102

Description

@Md-Mushfiqur-Rahim123

unwrap() ICE in orphan check error reporting when uncovered set is empty

Summary

In compiler/rustc_hir_analysis/src/coherence/orphan.rs, there is an unwrap() on a value that can be None, causing an ICE. The developer's own FIXME confirms: "This is very likely reachable."

Deep Analysis

The Bug — Two Correlated Failures

There are two linked FIXME sites in the orphan check pipeline that both share the same root cause:

Site 1 — Line 345-346 (orphan_check function):

// FIXME(fmease): This is very likely reachable.
debug_assert!(!collector.uncovered_params.is_empty());

Site 2 — Line 483 (emit_orphan_check_error function):

reported.unwrap() // FIXME(fmease): This is very likely reachable.

In release builds, debug_assert! is a no-op, so Site 1 silently passes. Then the empty set propagates downstream to Site 2, where unwrap() panics → ICE.

Full Call Chain

Here is the exact data flow from error detection to crash:

orphan_check_impl()                           // Entry: line 21
  │
  ├─ orphan_check(Proper)                     // line 28 — first check
  │    │
  │    ├─ orphan_check_trait_ref()            // solver in coherence.rs:221
  │    │    │
  │    │    └─ Returns Err(UncoveredTyParams { uncovered: Ty, .. })
  │    │       // `uncovered` is a SINGLE Ty (inference variable)
  │    │
  │    └─ map_err closure                     // line 340
  │         │
  │         ├─ UncoveredTyParamCollector visits the Ty
  │         │    ├─ Checks ty.has_type_flags(HAS_TY_INFER)  → true
  │         │    ├─ Matches ty::Infer(ty::TyVar(vid))
  │         │    └─ origin.param_def_id → ⚠️ CAN BE None!
  │         │       (when the infer var does NOT originate from a generic param)
  │         │
  │         ├─ debug_assert!(!empty) → NO-OP in release  ← Site 1
  │         └─ Returns UncoveredTyParams { uncovered: EMPTY set, .. }
  │
  ├─ orphan_check(Compat)                     // line 30 — fallback
  │    └─ Returns Err(same error)
  │
  └─ emit_orphan_check_error()                // line 39
       │
       └─ UncoveredTyParams branch            // line 469
            │
            ├─ for param_def_id in uncovered  // EMPTY → loop body never runs
            │    └─ reported.get_or_insert(...)  ← never called
            │
            └─ reported.unwrap()              // None → PANIC! ← Site 2

Root Cause

The UncoveredTyParamCollector visitor (line 520-544) only collects inference variables that have origin.param_def_id == Some(...):

fn visit_ty(&mut self, ty: Ty<'tcx>) -> Self::Result {
    // ...
    let origin = self.infcx.type_var_origin(vid);
    if let Some(def_id) = origin.param_def_id {  // ← CAN FAIL
        self.uncovered_params.insert(def_id);
    }
    // if None: silently skips! No insertion! Set stays empty!
}

The param_def_id is None when the type inference variable was created internally by the compiler (e.g., during normalization, method resolution, or obligation processing) rather than from a user-written generic parameter. The upstream orphan_check_trait_ref (in coherence.rs:241-252) detects any inference variable as an uncovered ty param — it doesn't check whether it traces back to a user-written generic.

So the invariant "if there's an uncovered infer var, the collector will always find a param" is wrong.

When This Can Happen

The orphan_check function on line 301 creates fresh inference variables:

let infcx = tcx.infer_ctxt().build(TypingMode::Coherence);
let args = infcx.fresh_args_for_item(cause.span, impl_def_id.to_def_id());

These are then lazily normalized (line 306-329). If normalization produces a new inference variable (one not originating from a generic param), and that variable is detected as uncovered by the solver, the collector will find it but skip it (no param_def_id), leaving the set empty.

This is possible with:

  • Projection normalization that creates auxiliary infer vars
  • Complex trait impls involving associated types and normalization failures
  • Edge cases in the next-gen trait solver (-Znext-solver)

Fix Applied

I've applied both fixes in compiler/rustc_hir_analysis/src/coherence/orphan.rs:

Change 1: debug_assert!assert! (line 345)

-            // FIXME(fmease): This is very likely reachable.
-            debug_assert!(!collector.uncovered_params.is_empty());
+            assert!(
+                !collector.uncovered_params.is_empty(),
+                "orphan check: uncovered ty params should not be empty after collection"
+            );

Rationale: If this invariant is truly expected to hold, it should be enforced in release builds too. If it can fail (as fmease suspects), then the assert will produce a clear, actionable panic message instead of a cryptic unwrap() failure 140 lines downstream.

Change 2: unwrap()bug!() (line 483)

-            reported.unwrap() // FIXME(fmease): This is very likely reachable.
+            reported.unwrap_or_else(|| {
+                bug!("orphan check: uncovered ty params was empty after collection")
+            })

Rationale: bug!() produces a proper ICE diagnostic with file/line info and a "please report this" message — much better than a raw unwrap() panic.

Meta

  • File: compiler/rustc_hir_analysis/src/coherence/orphan.rs
  • Lines: 345-348, 485-487
  • Severity: ICE (crash) in release builds
  • Labels: C-bug, I-ICE, A-coherence, E-easy
  • cc: @fmease (author of the original FIXME)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions