Repository navigation
Description
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)
Activity
fmease commentedon Apr 10, 2026
These asserts are indeed reachable or rather triggerable, not just very likely so; these source code comments aren't up to date. I've already opened an issue containing actual reproducers: #136188.
I'm not sure what to make out of your report. These FIXMEs will get addressed by PR #135910, I just need to get to it again. I don't think it makes sense to touch any of these asserts, it's simply not worth it, i.e., making "cosmetic" changes to them.