normalizes-to rework rigid alias handling

This commit is contained in:
lcnr 2025-02-11 10:37:04 +01:00 committed by Michael Goulet
parent c241e14650
commit de273e459e
7 changed files with 77 additions and 99 deletions

View file

@ -797,11 +797,12 @@ where
/// treat the alias as rigid.
///
/// See trait-system-refactor-initiative#124 for more details.
#[instrument(level = "debug", skip(self), ret)]
#[instrument(level = "debug", skip(self, inject_normalize_to_rigid_candidate), ret)]
pub(super) fn merge_candidates(
&mut self,
proven_via: Option<TraitGoalProvenVia>,
candidates: Vec<Candidate<I>>,
inject_normalize_to_rigid_candidate: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResult<I>,
) -> QueryResult<I> {
let Some(proven_via) = proven_via else {
// We don't care about overflow. If proving the trait goal overflowed, then
@ -818,13 +819,27 @@ where
// FIXME(const_trait_impl): should this behavior also be used by
// constness checking. Doing so is *at least theoretically* breaking,
// see github.com/rust-lang/rust/issues/133044#issuecomment-2500709754
TraitGoalProvenVia::ParamEnv | TraitGoalProvenVia::AliasBound => candidates
TraitGoalProvenVia::ParamEnv | TraitGoalProvenVia::AliasBound => {
let mut candidates_from_env: Vec<_> = candidates
.iter()
.filter(|c| {
matches!(c.source, CandidateSource::AliasBound | CandidateSource::ParamEnv(_))
matches!(
c.source,
CandidateSource::AliasBound | CandidateSource::ParamEnv(_)
)
})
.map(|c| c.result)
.collect(),
.collect();
// If the trait goal has been proven by using the environment, we want to treat
// aliases as rigid if there are no applicable projection bounds in the environment.
if candidates_from_env.is_empty() {
if let Ok(response) = inject_normalize_to_rigid_candidate(self) {
candidates_from_env.push(response);
}
}
candidates_from_env
}
TraitGoalProvenVia::Misc => candidates.iter().map(|c| c.result).collect(),
};

View file

@ -405,6 +405,6 @@ where
goal.with(ecx.cx(), goal.predicate.trait_ref);
ecx.compute_trait_goal(trait_goal)
})?;
self.merge_candidates(proven_via, candidates)
self.merge_candidates(proven_via, candidates, |_ecx| Err(NoSolution))
}
}

View file

@ -30,75 +30,26 @@ where
) -> QueryResult<I> {
self.set_is_normalizes_to_goal();
debug_assert!(self.term_is_fully_unconstrained(goal));
let normalize_result = self
.probe(|&result| ProbeKind::TryNormalizeNonRigid { result })
.enter(|this| this.normalize_at_least_one_step(goal));
match normalize_result {
Ok(res) => Ok(res),
Err(NoSolution) => {
self.probe(|&result| ProbeKind::RigidAlias { result }).enter(|this| {
let Goal { param_env, predicate: NormalizesTo { alias, term } } = goal;
this.add_rigid_constraints(param_env, alias)?;
this.relate_rigid_alias_non_alias(param_env, alias, ty::Invariant, term)?;
this.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
})
}
}
}
/// Register any obligations that are used to validate that an alias should be
/// treated as rigid.
///
/// An alias may be considered rigid if it fails normalization, but we also don't
/// want to consider aliases that are not well-formed to be rigid simply because
/// they fail normalization.
///
/// For example, some `<T as Trait>::Assoc` where `T: Trait` does not hold, or an
/// opaque type whose hidden type doesn't actually satisfy the opaque item bounds.
fn add_rigid_constraints(
&mut self,
param_env: I::ParamEnv,
rigid_alias: ty::AliasTerm<I>,
) -> Result<(), NoSolution> {
let cx = self.cx();
match rigid_alias.kind(cx) {
// Projections are rigid only if their trait ref holds,
// and the GAT where-clauses hold.
ty::AliasTermKind::ProjectionTy | ty::AliasTermKind::ProjectionConst => {
let trait_ref = rigid_alias.trait_ref(cx);
self.add_goal(GoalSource::AliasWellFormed, Goal::new(cx, param_env, trait_ref));
Ok(())
}
ty::AliasTermKind::OpaqueTy => {
if self.opaque_type_is_rigid(rigid_alias.def_id) {
Ok(())
} else {
Err(NoSolution)
}
}
// FIXME(generic_const_exprs): we would need to support generic consts here
ty::AliasTermKind::UnevaluatedConst => Err(NoSolution),
// Inherent and weak types are never rigid. This type must not be well-formed.
ty::AliasTermKind::WeakTy | ty::AliasTermKind::InherentTy => Err(NoSolution),
}
}
/// Normalize the given alias by at least one step. If the alias is rigid, this
/// returns `NoSolution`.
#[instrument(level = "trace", skip(self), ret)]
fn normalize_at_least_one_step(&mut self, goal: Goal<I, NormalizesTo<I>>) -> QueryResult<I> {
let cx = self.cx();
match goal.predicate.alias.kind(cx) {
ty::AliasTermKind::ProjectionTy | ty::AliasTermKind::ProjectionConst => {
let candidates = self.assemble_and_evaluate_candidates(goal);
let trait_ref = goal.predicate.alias.trait_ref(cx);
let (_, proven_via) =
self.probe(|_| ProbeKind::ShadowedEnvProbing).enter(|ecx| {
let trait_goal: Goal<I, ty::TraitPredicate<I>> =
goal.with(cx, goal.predicate.alias.trait_ref(cx));
let trait_goal: Goal<I, ty::TraitPredicate<I>> = goal.with(cx, trait_ref);
ecx.compute_trait_goal(trait_goal)
})?;
self.merge_candidates(proven_via, candidates)
self.merge_candidates(proven_via, candidates, |ecx| {
ecx.probe(|&result| ProbeKind::RigidAlias { result }).enter(|this| {
this.structurally_instantiate_normalizes_to_term(
goal,
goal.predicate.alias,
);
this.add_goal(GoalSource::AliasWellFormed, goal.with(cx, trait_ref));
this.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
})
})
}
ty::AliasTermKind::InherentTy => self.normalize_inherent_associated_type(goal),
ty::AliasTermKind::OpaqueTy => self.normalize_opaque_type(goal),
@ -120,6 +71,17 @@ where
self.eq(goal.param_env, goal.predicate.term, term)
.expect("expected goal term to be fully unconstrained");
}
/// Unlike `instantiate_normalizes_to_term` this instantiates the expected term
/// with a rigid alias. Using this is pretty much always wrong.
pub fn structurally_instantiate_normalizes_to_term(
&mut self,
goal: Goal<I, NormalizesTo<I>>,
term: ty::AliasTerm<I>,
) {
self.relate_rigid_alias_non_alias(goal.param_env, term, ty::Invariant, goal.predicate.term)
.expect("expected goal term to be fully unconstrained");
}
}
impl<D, I> assembly::GoalKind<D> for NormalizesTo<I>
@ -850,12 +812,14 @@ where
todo!("discr subgoal...")
}
// We do not call `Ty::discriminant_ty` on alias, param, or placeholder
// types, which return `<self_ty as DiscriminantKind>::Discriminant`
// (or ICE in the case of placeholders). Projecting a type to itself
// is never really productive.
// Given an alias, parameter, or placeholder we add an impl candidate normalizing to a rigid
// alias. In case there's a where-bound further constraining this alias it is preferred over
// this impl candidate anyways. It's still a bit scuffed.
ty::Alias(_, _) | ty::Param(_) | ty::Placeholder(..) => {
return Err(NoSolution);
return ecx.probe_builtin_trait_candidate(BuiltinImplSource::Misc).enter(|ecx| {
ecx.structurally_instantiate_normalizes_to_term(goal, goal.predicate.alias);
ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
});
}
ty::Infer(ty::TyVar(_) | ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_))
@ -902,12 +866,14 @@ where
todo!()
}
// We do not call `Ty::async_destructor_ty` on alias, param, or placeholder
// types, which return `<self_ty as AsyncDestruct>::AsyncDestructor`
// (or ICE in the case of placeholders). Projecting a type to itself
// is never really productive.
// Given an alias, parameter, or placeholder we add an impl candidate normalizing to a rigid
// alias. In case there's a where-bound further constraining this alias it is preferred over
// this impl candidate anyways. It's still a bit scuffed.
ty::Alias(_, _) | ty::Param(_) | ty::Placeholder(..) => {
return Err(NoSolution);
return ecx.probe_builtin_trait_candidate(BuiltinImplSource::Misc).enter(|ecx| {
ecx.structurally_instantiate_normalizes_to_term(goal, goal.predicate.alias);
ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
});
}
ty::Infer(ty::TyVar(_) | ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_))

View file

@ -35,14 +35,15 @@ where
self.evaluate_added_goals_and_make_canonical_response(Certainty::AMBIGUOUS)
}
TypingMode::Analysis { defining_opaque_types } => {
let Some(def_id) = opaque_ty.def_id.as_local() else {
return Err(NoSolution);
let Some(def_id) = opaque_ty
.def_id
.as_local()
.filter(|&def_id| defining_opaque_types.contains(&def_id))
else {
self.structurally_instantiate_normalizes_to_term(goal, goal.predicate.alias);
return self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
};
if !defining_opaque_types.contains(&def_id) {
return Err(NoSolution);
}
// FIXME: This may have issues when the args contain aliases...
match uses_unique_placeholders_ignoring_regions(self.cx(), opaque_ty.args) {
Err(NotUniqueParam::NotParam(param)) if param.is_non_region_infer() => {
@ -97,15 +98,16 @@ where
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
TypingMode::PostBorrowckAnalysis { defined_opaque_types } => {
let Some(def_id) = opaque_ty.def_id.as_local() else {
return Err(NoSolution);
let Some(def_id) = opaque_ty
.def_id
.as_local()
.filter(|&def_id| defined_opaque_types.contains(&def_id))
else {
self.structurally_instantiate_normalizes_to_term(goal, goal.predicate.alias);
return self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
};
if !defined_opaque_types.contains(&def_id) {
return Err(NoSolution);
}
let actual = cx.type_of(opaque_ty.def_id).instantiate(cx, opaque_ty.args);
let actual = cx.type_of(def_id.into()).instantiate(cx, opaque_ty.args);
// FIXME: Actually use a proper binder here instead of relying on `ReErased`.
//
// This is also probably unsound or sth :shrug:

View file

@ -300,7 +300,6 @@ impl<'a, 'tcx> InspectGoal<'a, 'tcx> {
inspect::ProbeKind::NormalizedSelfTyAssembly
| inspect::ProbeKind::UnsizeAssembly
| inspect::ProbeKind::Root { .. }
| inspect::ProbeKind::TryNormalizeNonRigid { .. }
| inspect::ProbeKind::TraitCandidate { .. }
| inspect::ProbeKind::OpaqueTypeStorageLookup { .. }
| inspect::ProbeKind::RigidAlias { .. } => {
@ -325,7 +324,6 @@ impl<'a, 'tcx> InspectGoal<'a, 'tcx> {
// We add a candidate even for the root evaluation if there
// is only one way to prove a given goal, e.g. for `WellFormed`.
inspect::ProbeKind::Root { result }
| inspect::ProbeKind::TryNormalizeNonRigid { result }
| inspect::ProbeKind::TraitCandidate { source: _, result }
| inspect::ProbeKind::OpaqueTypeStorageLookup { result }
| inspect::ProbeKind::RigidAlias { result } => {

View file

@ -175,8 +175,7 @@ fn to_selection<'tcx>(
span_bug!(span, "didn't expect to select an unknowable candidate")
}
},
ProbeKind::TryNormalizeNonRigid { result: _ }
| ProbeKind::NormalizedSelfTyAssembly
ProbeKind::NormalizedSelfTyAssembly
| ProbeKind::UnsizeAssembly
| ProbeKind::UpcastProjectionCompatibility
| ProbeKind::OpaqueTypeStorageLookup { result: _ }

View file

@ -111,8 +111,6 @@ pub enum ProbeStep<I: Interner> {
pub enum ProbeKind<I: Interner> {
/// The root inference context while proving a goal.
Root { result: QueryResult<I> },
/// Trying to normalize an alias by at least one step in `NormalizesTo`.
TryNormalizeNonRigid { result: QueryResult<I> },
/// Probe entered when normalizing the self ty during candidate assembly
NormalizedSelfTyAssembly,
/// A candidate for proving a trait or alias-relate goal.