Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
159 changes: 87 additions & 72 deletions compiler/rustc_hir_typeck/src/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,90 +45,102 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
}
}

/// Tries to apply a fallback to all unresolved variables.
///
/// - Unconstrained ints are replaced with `i32`.
///
/// - Unconstrained floats are replaced with `f64`, except when there is a trait predicate
/// `f32: From<{float}>`, in which case `f32` is used as the fallback instead.
Comment on lines +52 to +53

@lcnr lcnr Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you change this comment to name the FCW we emit here so that we update this comment when removing the fallback

View changes since the review

///
/// - Non-numerics may get replaced with `()` or `!`, depending on how they
/// were categorized by [`Self::calculate_diverging_fallback`], crate's
/// edition, and the setting of `#![rustc_never_type_options(fallback = ...)]`.
///
/// Fallback becomes very dubious if we have encountered type-checking errors.
/// In that case, all variables fallback to Error.
///
/// Sets [`FnCtxt::diverging_fallback_has_occurred`] if never type fallback
/// is performed during this call.
///
/// Returns `true` if *any* kind of fallback has occurred during this call.
fn fallback_types(&self) -> bool {
// Check if we have any unresolved variables. If not, no need for fallback.
let unresolved_variables = self.unresolved_variables();
let (unresolved_ty, unresolved_int, unresolved_float) = self.unresolved_root_variables();

if unresolved_variables.is_empty() {
// Check if we have any unresolved variables. If not, no need for fallback.
if unresolved_ty.is_empty() && unresolved_int.is_empty() && unresolved_float.is_empty() {
return false;
}

let (diverging_fallback, diverging_fallback_ty) =
self.calculate_diverging_fallback(&unresolved_variables);
let fallback_to_f32 = self.calculate_fallback_to_f32(&unresolved_variables);
self.calculate_diverging_fallback(&unresolved_ty);
let fallback_to_f32 = self.calculate_fallback_to_f32(&unresolved_float);

// We do fallback in two passes, to try to generate
// better error messages.
// The first time, we do *not* replace opaque types.
let mut fallback_occurred = false;
for ty in unresolved_variables {
debug!("unsolved_variable = {:?}", ty);

for vid in unresolved_ty {
fallback_occurred |= self.fallback_if_possible(
ty,
&diverging_fallback,
diverging_fallback_ty,
&fallback_to_f32,
vid,
|| {
diverging_fallback.contains(&vid).then(|| {
self.diverging_fallback_has_occurred.set(true);
diverging_fallback_ty
})
},
|vid| (Ty::new_var(self.tcx, vid), self.type_var_origin(vid).span),
);
}

for vid in unresolved_int {
fallback_occurred |= self.fallback_if_possible(
vid,
|| Some(self.tcx.types.i32),
// Int variables have no origin?..
|vid| (Ty::new_int_var(self.tcx, vid), DUMMY_SP),
);
}

for vid in unresolved_float {
fallback_occurred |= self.fallback_if_possible(
vid,
|| {
Some(if fallback_to_f32.contains(&vid) {
self.tcx.types.f32
} else {
self.tcx.types.f64
})
},
|vid| (Ty::new_float_var(self.tcx, vid), self.float_var_origin(vid).span),
);
}

fallback_occurred
}

/// Tries to apply a fallback to `ty` if it is an unsolved variable.
///
/// - Unconstrained ints are replaced with `i32`.
/// Applies fallback to `vid`, if possible.
///
/// - Unconstrained floats are replaced with `f64`, except when there is a trait predicate
/// `f32: From<{float}>`, in which case `f32` is used as the fallback instead.
///
/// - Non-numerics may get replaced with `()` or `!`, depending on how they
/// were categorized by [`Self::calculate_diverging_fallback`], crate's
/// edition, and the setting of `#![rustc_never_type_options(fallback = ...)]`.
/// - If `self.tainted_by_errors()` unifies the type represented by `vid` with error
/// - Otherwise, if `fallback` returns `Some`, unifies it with the output of `fallback`
/// - Otherwise, does nothing
///
/// Fallback becomes very dubious if we have encountered
/// type-checking errors. In that case, fallback to Error.
///
/// Sets [`FnCtxt::diverging_fallback_has_occurred`] if never type fallback
/// is performed during this call.
fn fallback_if_possible(
/// Returns whatever fallback has been applied.
fn fallback_if_possible<V>(
&self,
ty: Ty<'tcx>,
diverging_fallback: &UnordSet<Ty<'tcx>>,
diverging_fallback_ty: Ty<'tcx>,
fallback_to_f32: &UnordSet<FloatVid>,
vid: V,
fallback: impl FnOnce() -> Option<Ty<'tcx>>,
vid_to_ty_and_span: impl FnOnce(V) -> (Ty<'tcx>, Span),
) -> bool {
// Careful: we do NOT shallow-resolve `ty`. We know that `ty`
// is an unsolved variable, and we determine its fallback
// based solely on how it was created, not what other type
// variables it may have been unified with since then.
//
// The reason this matters is that other attempts at fallback
// may (in principle) conflict with this fallback, and we wish
// to generate a type error in that case. (However, this
// actually isn't true right now, because we're only using the
// builtin fallback rules. This would be true if we were using
// user-supplied fallbacks. But it's still useful to write the
// code to detect bugs.)
//
// (Note though that if we have a general type variable `?T`
// that is then unified with an integer type variable `?I`
// that ultimately never gets resolved to a special integral
// type, `?T` is not considered unsolved, but `?I` is. The
// same is true for float variables.)
let fallback = match ty.kind() {
_ if let Some(e) = self.tainted_by_errors() => Ty::new_error(self.tcx, e),
ty::Infer(ty::IntVar(_)) => self.tcx.types.i32,
ty::Infer(ty::FloatVar(vid)) if fallback_to_f32.contains(vid) => self.tcx.types.f32,
ty::Infer(ty::FloatVar(_)) => self.tcx.types.f64,
_ if diverging_fallback.contains(&ty) => {
self.diverging_fallback_has_occurred.set(true);
diverging_fallback_ty
}
_ => return false,
let fallback = if let Some(e) = self.tainted_by_errors() {
Ty::new_error(self.tcx, e)
} else if let Some(fallback) = fallback() {
fallback
} else {
return false;
};
debug!("fallback_if_possible(ty={:?}): defaulting to `{:?}`", ty, fallback);

let span = ty.ty_vid().map_or(DUMMY_SP, |vid| self.infcx.type_var_origin(vid).span);
let (ty, span) = vid_to_ty_and_span(vid);
self.demand_eqtype(span, ty, fallback);
true
}
Expand All @@ -146,15 +158,19 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
///
/// foo(1.0);
/// ```
fn calculate_fallback_to_f32(&self, unresolved_variables: &[Ty<'tcx>]) -> UnordSet<FloatVid> {
fn calculate_fallback_to_f32(
&self,
unresolved_variables: &[ty::FloatVid],
) -> UnordSet<FloatVid> {
// Short-circuit: if no unresolved variable is a float, no f32 fallback can apply,
// so we can skip the (potentially very expensive) work in `from_float_for_f32_root_vids`.
// Under the new solver, that function walks `visit_proof_tree` for every pending
// obligation, which is O(N × proof_tree_size) and can dominate type-checking on crates
// with many large pending obligations and no f32 involvement.
if unresolved_variables.iter().all(|ty| ty.float_vid().is_none()) {
if unresolved_variables.is_empty() {
return UnordSet::new();
}

let roots: UnordSet<ty::FloatVid> = self.from_float_for_f32_root_vids();
if roots.is_empty() {
// Most functions have no `f32: From<{float}>` predicates, so short-circuit and return
Expand All @@ -166,10 +182,10 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
// top of that function for details.
let fallback_to_f32 = unresolved_variables
.iter()
.flat_map(|ty| ty.float_vid())
.filter(|vid| roots.contains(&self.root_float_var(*vid)))
.inspect(|vid| {
let origin = self.float_var_origin(*vid);
.copied()
.filter(|&vid| roots.contains(&self.root_float_var(vid)))
.inspect(|&vid| {
let origin = self.float_var_origin(vid);
// Show the entire literal in the suggestion to make it clearer.
let mut literal = self.tcx.sess.source_map().span_to_snippet(origin.span).ok();
// A `.` at the end of the literal is no longer necessary if `f32` is explicitly specified
Expand All @@ -195,8 +211,8 @@ impl<'tcx> FnCtxt<'_, 'tcx> {

fn calculate_diverging_fallback(
&self,
unresolved_variables: &[Ty<'tcx>],
) -> (UnordSet<Ty<'tcx>>, Ty<'tcx>) {
unresolved_variables: &[ty::TyVid],
) -> (UnordSet<ty::TyVid>, Ty<'tcx>) {
debug!("calculate_diverging_fallback({:?})", unresolved_variables);

let diverging_fallback_ty = match self.diverging_fallback_behavior {
Expand All @@ -214,7 +230,7 @@ impl<'tcx> FnCtxt<'_, 'tcx> {

// Extract the unsolved type inference variable vids; note that some
// unsolved variables are integer/float variables and are excluded.
let unsolved_vids = unresolved_variables.iter().filter_map(|ty| ty.ty_vid());
let unsolved_vids = unresolved_variables.iter();

// Compute the diverging root vids D -- that is, the root vid of
// those type variables that (a) are the target of a coercion from
Expand All @@ -241,7 +257,7 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
// we find later that they are *also* reachable from some
// other type variable outside this set.
let mut diverging_vids = vec![];
for unsolved_vid in unsolved_vids {
for &unsolved_vid in unsolved_vids {
let root_vid = self.root_var(unsolved_vid);
debug!(
"calculate_diverging_fallback: unsolved_vid={:?} root_vid={:?} diverges={:?}",
Expand Down Expand Up @@ -271,7 +287,6 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
);

for &diverging_vid in &diverging_vids {
let diverging_ty = Ty::new_var(self.tcx, diverging_vid);
let root_vid = self.root_var(diverging_vid);

self.lint_never_type_fallback_flowing_into_unsafe_code(
Expand All @@ -280,7 +295,7 @@ impl<'tcx> FnCtxt<'_, 'tcx> {
root_vid,
);

diverging_fallback.insert(diverging_ty);
diverging_fallback.insert(diverging_vid);
}

(diverging_fallback, diverging_fallback_ty)
Expand Down
36 changes: 17 additions & 19 deletions compiler/rustc_infer/src/infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,27 +752,25 @@ impl<'tcx> InferCtxt<'tcx> {
}
}

pub fn unresolved_variables(&self) -> Vec<Ty<'tcx>> {
pub fn unresolved_root_variables(&self) -> (Vec<TyVid>, Vec<ty::IntVid>, Vec<ty::FloatVid>) {
let mut inner = self.inner.borrow_mut();
let mut vars: Vec<Ty<'_>> = inner
.type_variables()
.unresolved_variables()
.into_iter()
.map(|t| Ty::new_var(self.tcx, t))
let ty = inner.type_variables().unresolved_root_variables().into_iter().collect();
let int = (0..inner.int_unification_table().len())
.map(|i| ty::IntVid::from_usize(i))
.filter(|&vid| {
inner.int_unification_table().probe_value(vid).is_unknown()
&& inner.int_unification_table().find(vid) == vid
})
.collect();
vars.extend(
(0..inner.int_unification_table().len())
.map(|i| ty::IntVid::from_usize(i))
.filter(|&vid| inner.int_unification_table().probe_value(vid).is_unknown())
.map(|v| Ty::new_int_var(self.tcx, v)),
);
vars.extend(
(0..inner.float_unification_table().len())
.map(|i| ty::FloatVid::from_usize(i))
.filter(|&vid| inner.float_unification_table().probe_value(vid).is_unknown())
.map(|v| Ty::new_float_var(self.tcx, v)),
);
vars
let float = (0..inner.float_unification_table().len())
.map(|i| ty::FloatVid::from_usize(i))
.filter(|&vid| {
inner.float_unification_table().probe_value(vid).is_unknown()
&& inner.float_unification_table().find(vid) == vid
})
.collect();

(ty, int, float)
}

#[instrument(skip(self), level = "debug")]
Expand Down
8 changes: 5 additions & 3 deletions compiler/rustc_infer/src/infer/type_variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,14 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> {
(range.clone(), range.map(|index| self.var_origin(index)).collect())
}

/// Returns indices of all variables that are not yet
/// instantiated.
pub(crate) fn unresolved_variables(&mut self) -> Vec<ty::TyVid> {
/// Returns indices of all root variables that are not yet instantiated.
pub(crate) fn unresolved_root_variables(&mut self) -> Vec<ty::TyVid> {
(0..self.num_vars())
.filter_map(|i| {
let vid = ty::TyVid::from_usize(i);
if self.root_var(vid) != vid {
return None;
}
match self.probe(vid) {
TypeVariableValue::Unknown { .. } => Some(vid),
TypeVariableValue::Known { .. } => None,
Expand Down