Skip to content
4 changes: 2 additions & 2 deletions compiler/rustc_hir_analysis/src/check/compare_impl_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,7 @@ fn check_region_late_boundedness<'tcx>(
.inner
.borrow_mut()
.unwrap_region_constraints()
.opportunistic_resolve_var(tcx, vid)
.shallow_resolve_region_var(tcx, vid)
&& let ty::ReLateParam(ty::LateParamRegion {
kind: ty::LateParamRegionKind::Named(trait_param_def_id),
..
Expand All @@ -1321,7 +1321,7 @@ fn check_region_late_boundedness<'tcx>(
.inner
.borrow_mut()
.unwrap_region_constraints()
.opportunistic_resolve_var(tcx, vid)
.shallow_resolve_region_var(tcx, vid)
&& let ty::ReLateParam(ty::LateParamRegion {
kind: ty::LateParamRegionKind::Named(impl_param_def_id),
..
Expand Down
8 changes: 4 additions & 4 deletions compiler/rustc_infer/src/infer/canonical/canonicalizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ impl CanonicalizeMode for CanonicalizeQueryResponse {
.inner
.borrow_mut()
.unwrap_region_constraints()
.opportunistic_resolve_var(canonicalizer.tcx, vid);
.shallow_resolve_region_var(canonicalizer.tcx, vid);

@lcnr lcnr Aug 12, 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.

we don't reuse r here if the root doesn't change 🤔 feels like doing so would be good for perf 🤔

View changes since the review

debug!(
"canonical: region var found with vid {vid:?}, \
opportunistically resolved to {r:?}",
Expand All @@ -182,7 +182,7 @@ impl CanonicalizeMode for CanonicalizeQueryResponse {
.inner
.borrow_mut()
.unwrap_region_constraints()
.probe_value(vid)
.try_resolve_region_var(vid)
.unwrap_err();
canonicalizer.canonical_var_for_region(CanonicalVarKind::Region(universe), r)
}
Expand Down Expand Up @@ -362,15 +362,15 @@ impl<'cx, 'tcx> TypeFolder<TyCtxt<'tcx>> for Canonicalizer<'cx, 'tcx> {
}

ty::Infer(ty::IntVar(vid)) => {
let nt = self.infcx.unwrap().opportunistic_resolve_int_var(vid);

@jdonszelmann jdonszelmann Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

renames all the opportunistic_* functions with simply shallow_resolve_*. I've done this in many places. I've added a lot of docs to all the resolve methods, which I think makes it super clear that resolving is always an opportunistic process that doesn't necessarily resolve all variables, simply because it can't always.

From the perspective of a new contributor, they'll see a resolve_* function for the first time, go to its docs, and learn that the purpose of all resolve_* methods is to opportunistically resolve variables.

Since the behavior is the same for all the resolve_* functions I think that will actually make things clearer than randomly calling some of them "opportunistic" even when the others are inherently also opportunistic.

View changes since the review

let nt = self.infcx.unwrap().shallow_resolve_int_var(vid);
if nt != t {
return self.fold_ty(nt);
} else {
self.canonicalize_ty_var(CanonicalVarKind::Int, t)
}
}
ty::Infer(ty::FloatVar(vid)) => {
let nt = self.infcx.unwrap().opportunistic_resolve_float_var(vid);
let nt = self.infcx.unwrap().shallow_resolve_float_var(vid);
if nt != t {
return self.fold_ty(nt);
} else {
Expand Down
11 changes: 7 additions & 4 deletions compiler/rustc_infer/src/infer/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl<'tcx> rustc_type_ir::InferCtxtLike for InferCtxt<'tcx> {
}

fn universe_of_lt(&self, lt: ty::RegionVid) -> Option<ty::UniverseIndex> {
match self.inner.borrow_mut().unwrap_region_constraints().probe_value(lt) {
match self.inner.borrow_mut().unwrap_region_constraints().try_resolve_region_var(lt) {
Err(universe) => Some(universe),
Ok(_) => None,
}
Expand Down Expand Up @@ -131,11 +131,11 @@ impl<'tcx> rustc_type_ir::InferCtxtLike for InferCtxt<'tcx> {
}

fn opportunistic_resolve_int_var(&self, vid: ty::IntVid) -> Ty<'tcx> {
self.opportunistic_resolve_int_var(vid)
self.shallow_resolve_int_var(vid)
}

fn opportunistic_resolve_float_var(&self, vid: ty::FloatVid) -> Ty<'tcx> {
self.opportunistic_resolve_float_var(vid)
self.shallow_resolve_float_var(vid)
}

fn opportunistic_resolve_ct_var(&self, vid: ty::ConstVid) -> ty::Const<'tcx> {
Expand All @@ -146,7 +146,10 @@ impl<'tcx> rustc_type_ir::InferCtxtLike for InferCtxt<'tcx> {
}

fn opportunistic_resolve_lt_var(&self, vid: ty::RegionVid) -> ty::Region<'tcx> {
self.inner.borrow_mut().unwrap_region_constraints().opportunistic_resolve_var(self.tcx, vid)
self.inner
.borrow_mut()
.unwrap_region_constraints()
.shallow_resolve_region_var(self.tcx, vid)
}

fn ty_or_const_infer_var_changed(&self, var: TyOrConstInferVar) -> bool {
Expand Down
227 changes: 148 additions & 79 deletions compiler/rustc_infer/src/infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ use rustc_middle::traits::solve::Goal;
use rustc_middle::ty::error::{ExpectedFound, TypeError};
use rustc_middle::ty::{
self, BoundVarReplacerDelegate, ConstVid, FloatVid, GenericArg, GenericArgKind, GenericArgs,
GenericArgsRef, GenericParamDefKind, InferConst, OpaqueTypeKey, ProvisionalHiddenType,
PseudoCanonicalInput, RegionExt, Term, Ty, TyCtxt, TyVid, TypeFoldable, TypeFolder,
TypeSuperFoldable, TypeVisitable, TypeVisitableExt, TypingEnv, TypingMode, fold_regions,
GenericArgsRef, GenericParamDefKind, InferConst, InferTy, IntVid, OpaqueTypeKey,
ProvisionalHiddenType, PseudoCanonicalInput, RegionExt, Term, Ty, TyCtxt, TyVid, TypeFoldable,
TypeFolder, TypeSuperFoldable, TypeVisitable, TypeVisitableExt, TypingEnv, TypingMode,
fold_regions,
};
use rustc_span::{DUMMY_SP, Span, Symbol};
use rustc_type_ir::MayBeErased;
Expand Down Expand Up @@ -1214,10 +1215,10 @@ impl<'tcx> InferCtxt<'tcx> {
/// If `TyVar(vid)` resolves to a type, return that type. Else, return the
/// universe index of `TyVar(vid)`.
pub fn try_resolve_ty_var(&self, vid: TyVid) -> Result<Ty<'tcx>, ty::UniverseIndex> {
use self::type_variable::TypeVariableValue;
let value = self.inner.borrow_mut().type_variables().probe(vid);

match self.inner.borrow_mut().type_variables().probe(vid) {
TypeVariableValue::Known { value } => Ok(value),
match value {
TypeVariableValue::Known { value } => Ok(self.shallow_resolve_non_recursive(value)),

@jdonszelmann jdonszelmann Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this method (and the one below) now also does the recursive resolving shallow_resolve already did. No tests change here.

View changes since the review

TypeVariableValue::Unknown { universe } => Err(universe),
}
}
Expand All @@ -1227,76 +1228,157 @@ impl<'tcx> InferCtxt<'tcx> {
let (root, value) = self.inner.borrow_mut().type_variables().probe_with_root_vid(vid);

match value {
TypeVariableValue::Known { value } => Ok(value),
TypeVariableValue::Known { value } => Ok(self.shallow_resolve_non_recursive(value)),
TypeVariableValue::Unknown { universe: _ } => Err(root),
}
}

pub fn shallow_resolve(&self, ty: Ty<'tcx>) -> Ty<'tcx> {
if let ty::Infer(v) = *ty.kind() {
match v {
ty::TyVar(v) => {
// Not entirely obvious: if `typ` is a type variable,
// it can be resolved to an int/float variable, which
// can then be recursively resolved, hence the
// recursion. Note though that we prevent type
// variables from unifying to other type variables
// directly (though they may be embedded
// structurally), and we prevent cycles in any case,
// so this recursion should always be of very limited
// depth.
//
// Note: if these two lines are combined into one we get
// dynamic borrow errors on `self.inner`.
let (root_vid, value) =
self.inner.borrow_mut().type_variables().probe_with_root_vid(v);
value.known().map_or_else(
|| if root_vid == v { ty } else { Ty::new_var(self.tcx, root_vid) },
|t| self.shallow_resolve(t),
)
/// Resolve a type variable to a type, if known.
/// Otherwise return a type with the root vid in it.
///
/// Not entirely obvious:
/// It's possible for a type variable to resolve to an int/float variable.
/// When that happens, the int/float variable may itself already be resolved
/// to an int/float, which is the type we actually want to return, not the variable.
///
/// Only one step of this is ever possible. We never resolve type variables to other
/// type variables. Therefore, we use [`shallow_resolve_non_recursive`](Self::shallow_resolve_non_recursive),
/// to call into a version of shallow_resolve that only knows about int/float variables
/// and panics (and notably: doesn't recurse again) when it sees type variables.
/// That way the compiler knows the recursion can only ever go two deep, which helps performance.
Comment thread
jdonszelmann marked this conversation as resolved.
///
/// `ty` is a type that we may already have available, which represents the `TyVid`.
/// In cases where we do, this can aid performance.
#[inline(always)]
fn shallow_resolve_ty_var_with_ty(&self, v: TyVid, ty: Option<Ty<'tcx>>) -> Ty<'tcx> {

@jdonszelmann jdonszelmann Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

By taking an Option here, we can merge more methods' implementations. Doing this has 0 performance overhead, #[inline(always)] makes sure the callsites that always call with Some get optimized properly.

View changes since the review

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.

could we instead change this function to not take a ty and return and Option instead?

let (root_vid, value) = self.inner.borrow_mut().type_variables().inlined_probe_with_vid(v);
match value {
TypeVariableValue::Known { value } => self.shallow_resolve_non_recursive(value),
TypeVariableValue::Unknown { .. } => {
if root_vid == v
&& let Some(ty) = ty
{
ty
} else {
Ty::new_var(self.tcx, root_vid)
}
}
}
}

ty::IntVar(v) => {
let (root, value) =
self.inner.borrow_mut().int_unification_table().inlined_probe_key_value(v);
match value {
ty::IntVarValue::IntType(ty) => Ty::new_int(self.tcx, ty),
ty::IntVarValue::UintType(ty) => Ty::new_uint(self.tcx, ty),
ty::IntVarValue::Unknown => {
if root == v {
ty
} else {
Ty::new_int_var(self.tcx, root)
}
}
}
/// Resolve a type variable to an integer type, if known.
/// Otherwise return a type with the root int vid in it.
///
/// `ty` is a type that we may already have available, which represents the `IntVid`.
/// In cases where we do, this can aid performance.
#[inline(always)]
fn shallow_resolve_int_var_with_ty(&self, v: IntVid, ty: Option<Ty<'tcx>>) -> Ty<'tcx> {
let (root, value) =
self.inner.borrow_mut().int_unification_table().inlined_probe_key_value(v);
match value {
ty::IntVarValue::IntType(ty) => Ty::new_int(self.tcx, ty),
ty::IntVarValue::UintType(ty) => Ty::new_uint(self.tcx, ty),
ty::IntVarValue::Unknown => {
if root == v
&& let Some(ty) = ty
{
ty
} else {
Ty::new_int_var(self.tcx, root)
}
}
}
}

ty::FloatVar(v) => {
let (root, value) = self
.inner
.borrow_mut()
.float_unification_table()
.inlined_probe_key_value(v);
match value {
ty::FloatVarValue::Known(ty) => Ty::new_float(self.tcx, ty),
ty::FloatVarValue::Unknown => {
if root == v {
ty
} else {
Ty::new_float_var(self.tcx, root)
}
}
}
/// Resolve a type variable to a float type, if known.
/// Otherwise return a type with the root float vid in it.
///
/// `ty` is a type that we may already have available, which represents the `FloatVid`.
/// In cases where we do, this can aid performance.
#[inline(always)]
fn shallow_resolve_float_var_with_ty(&self, v: FloatVid, ty: Option<Ty<'tcx>>) -> Ty<'tcx> {
let (root, value) =
self.inner.borrow_mut().float_unification_table().inlined_probe_key_value(v);
match value {
ty::FloatVarValue::Known(ty) => Ty::new_float(self.tcx, ty),
ty::FloatVarValue::Unknown => {
if root == v
&& let Some(ty) = ty
{
ty
} else {
Ty::new_float_var(self.tcx, root)
}
}
}
}

ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_) => ty,
/// Shallow resolve a type/int infer var, panics on type variables.
///
/// See docs on [`shallow_resolve_ty_var`](Self::shallow_resolve_ty_var) for why this exists.
#[inline(never)]
// Cold because the case in which a tyvar resolves to an intvar which resolves to a type is
// quite rare. It's way more common for `shallow_resolve_non_recursive` to return ty.
#[cold]
fn shallow_resolve_infer_non_recursive(&self, infer: InferTy, ty: Ty<'tcx>) -> Ty<'tcx> {

@jdonszelmann jdonszelmann Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the non-recursive caase helps ~0.5% on local benchmarks. Not much, but still a bit.

View changes since the review

match infer {
ty::TyVar(_) => {
unreachable!()
}
Comment thread
jdonszelmann marked this conversation as resolved.
ty::IntVar(v) => self.shallow_resolve_int_var_with_ty(v, Some(ty)),
ty::FloatVar(v) => self.shallow_resolve_float_var_with_ty(v, Some(ty)),
ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_) => ty,
}
}

#[inline(always)]
fn shallow_resolve_infer(&self, infer: InferTy, ty: Ty<'tcx>) -> Ty<'tcx> {
match infer {
ty::TyVar(v) => self.shallow_resolve_ty_var_with_ty(v, Some(ty)),
ty::IntVar(v) => self.shallow_resolve_int_var_with_ty(v, Some(ty)),
ty::FloatVar(v) => self.shallow_resolve_float_var_with_ty(v, Some(ty)),
ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_) => ty,
}
}

/// Shallow resolve a type, panics on type variables.
/// See [`shallow_resolve`](Self::shallow_resolve) for more docs.
///
/// See docs on [`shallow_resolve_ty_var`](Self::shallow_resolve_ty_var) for why this alternate
/// version of shallow_resolve exists.
#[inline(always)]
fn shallow_resolve_non_recursive(&self, ty: Ty<'tcx>) -> Ty<'tcx> {
if let ty::Infer(infer) = *ty.kind() {
self.shallow_resolve_infer_non_recursive(infer, ty)
} else {
ty
}
}

/// Resolve a type variable. Resolving means the following:
///
/// - If a `Ty` is a rigid type (like, an integer, or some ADT), do nothing.
/// - If a `Ty` is a type infer variable, but has been equated with an actual type,
/// return that type.
/// - If a `Ty` is an int or float infer variable, and has been equated with an integer
/// or floating point type, return that type.
/// - If a `Ty` is any kind of infer variable that has been equated, but not yet with a rigid
/// type, then this set of equated variables forms an equivalence class. One of the variables
/// in that equivalent class is said to be the root variable, and resolving makes sure to
/// consistently return this root variable. This is beneficial for caching.
/// This behavior, of returning roots, changed in <https://github.com/rust-lang/rust/pull/158447>.
///
/// Otherwise, resolving simply does nothing.
///
/// The "shallow" part of the name refers to the fact that types may themselves contain more
/// type variables. e.g. The field types of a struct. `shallow_resolve` does not recurse into
/// these nested variables. If that's what you want, use [`resolve_vars_if_possible`](Self::resolve_vars_if_possible)
pub fn shallow_resolve(&self, ty: Ty<'tcx>) -> Ty<'tcx> {
if let ty::Infer(infer) = *ty.kind() { self.shallow_resolve_infer(infer, ty) } else { ty }
}

/// See docs on [`shallow_resolve`](Self::shallow_resolve) for more explanation.
/// It's the same, but for consts.
pub fn shallow_resolve_const(&self, ct: ty::Const<'tcx>) -> ty::Const<'tcx> {
match ct.kind() {
ty::ConstKind::Infer(infer_ct) => match infer_ct {
Expand All @@ -1323,6 +1405,8 @@ impl<'tcx> InferCtxt<'tcx> {
}
}

/// See docs on [`shallow_resolve`](Self::shallow_resolve) for more explanation.
/// It's the same, but for terms (types or consts).
pub fn shallow_resolve_term(&self, term: ty::Term<'tcx>) -> ty::Term<'tcx> {
match term.kind() {
ty::TermKind::Ty(ty) => self.shallow_resolve(ty).into(),
Expand Down Expand Up @@ -1359,29 +1443,14 @@ impl<'tcx> InferCtxt<'tcx> {

/// Resolves an int var to a rigid int type, if it was constrained to one,
/// or else the root int var in the unification table.
pub fn opportunistic_resolve_int_var(&self, vid: ty::IntVid) -> Ty<'tcx> {
let mut inner = self.inner.borrow_mut();
let value = inner.int_unification_table().probe_value(vid);
match value {
ty::IntVarValue::IntType(ty) => Ty::new_int(self.tcx, ty),
ty::IntVarValue::UintType(ty) => Ty::new_uint(self.tcx, ty),
ty::IntVarValue::Unknown => {
Ty::new_int_var(self.tcx, inner.int_unification_table().find(vid))
}
}
pub fn shallow_resolve_int_var(&self, vid: ty::IntVid) -> Ty<'tcx> {

@lcnr lcnr Aug 12, 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.

if we return None if the inner function doesn't make progress, could we always be explicit about reconstructing the type in the caller to make it explicit where we (likely unnecessarily) do so?

View changes since the review

self.shallow_resolve_int_var_with_ty(vid, None)
}

/// Resolves a float var to a rigid int type, if it was constrained to one,
/// or else the root float var in the unification table.
pub fn opportunistic_resolve_float_var(&self, vid: ty::FloatVid) -> Ty<'tcx> {
let mut inner = self.inner.borrow_mut();
let value = inner.float_unification_table().probe_value(vid);
match value {
ty::FloatVarValue::Known(ty) => Ty::new_float(self.tcx, ty),
ty::FloatVarValue::Unknown => {
Ty::new_float_var(self.tcx, inner.float_unification_table().find(vid))
}
}
pub fn shallow_resolve_float_var(&self, vid: ty::FloatVid) -> Ty<'tcx> {
self.shallow_resolve_float_var_with_ty(vid, None)
}

/// Where possible, replaces type/const variables in
Expand Down
Loading