Skip to content

Commit

Permalink
Rollup merge of #123363 - lcnr:normalizes-to-zero-to-inf, r=BoxyUwU
Browse files Browse the repository at this point in the history
change `NormalizesTo` to fully structurally normalize

notes in https://hackmd.io/wZ016dE4QKGIhrOnHLlThQ

need to also update the dev-guide once this PR lands. in short, the setup is now as follows:

`normalizes-to` internally implements one step normalization, applying that normalization to the `goal.predicate.term` causes the projected term to get recursively normalized. With this `normalizes-to` normalizes until the projected term is rigid, meaning that we normalize as many steps necessary, but at least 1.

To handle rigid aliases, we add another candidate only if the 1 to inf step normalization failed. With this `normalizes-to` is now full structural normalization. We can now change `AliasRelate` to simply emit `normalizes-to` goals for the rhs and lhs.

This avoids the concerns from rust-lang/trait-system-refactor-initiative#103 and generally feels cleaner
  • Loading branch information
jhpratt committed Apr 5, 2024
2 parents de2cb0d + 92b280c commit fcb0e9d
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 310 deletions.
7 changes: 2 additions & 5 deletions compiler/rustc_middle/src/traits/solve/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ pub enum ProbeStep<'tcx> {
/// used whenever there are multiple candidates to prove the
/// current goalby .
NestedProbe(Probe<'tcx>),
CommitIfOkStart,
CommitIfOkSuccess,
}

/// What kind of probe we're in. In case the probe represents a candidate, or
Expand All @@ -132,6 +130,8 @@ pub enum ProbeStep<'tcx> {
pub enum ProbeKind<'tcx> {
/// The root inference context while proving a goal.
Root { result: QueryResult<'tcx> },
/// Trying to normalize an alias by at least one stpe in `NormalizesTo`.
TryNormalizeNonRigid { result: QueryResult<'tcx> },
/// Probe entered when normalizing the self ty during candidate assembly
NormalizedSelfTyAssembly,
/// Some candidate to prove the current goal.
Expand All @@ -143,9 +143,6 @@ pub enum ProbeKind<'tcx> {
/// Used in the probe that wraps normalizing the non-self type for the unsize
/// trait, which is also structurally matched on.
UnsizeAssembly,
/// A call to `EvalCtxt::commit_if_ok` which failed, causing the work
/// to be discarded.
CommitIfOk,
/// During upcasting from some source object to target object type, used to
/// do a probe to find out what projection type(s) may be used to prove that
/// the source type upholds all of the target type's object bounds.
Expand Down
8 changes: 3 additions & 5 deletions compiler/rustc_middle/src/traits/solve/inspect/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ impl<'a, 'b> ProofTreeFormatter<'a, 'b> {
ProbeKind::Root { result } => {
write!(self.f, "ROOT RESULT: {result:?}")
}
ProbeKind::TryNormalizeNonRigid { result } => {
write!(self.f, "TRY NORMALIZE NON-RIGID: {result:?}")
}
ProbeKind::NormalizedSelfTyAssembly => {
write!(self.f, "NORMALIZING SELF TY FOR ASSEMBLY:")
}
Expand All @@ -109,9 +112,6 @@ impl<'a, 'b> ProofTreeFormatter<'a, 'b> {
ProbeKind::UpcastProjectionCompatibility => {
write!(self.f, "PROBING FOR PROJECTION COMPATIBILITY FOR UPCASTING:")
}
ProbeKind::CommitIfOk => {
write!(self.f, "COMMIT_IF_OK:")
}
ProbeKind::MiscCandidate { name, result } => {
write!(self.f, "CANDIDATE {name}: {result:?}")
}
Expand All @@ -132,8 +132,6 @@ impl<'a, 'b> ProofTreeFormatter<'a, 'b> {
}
ProbeStep::EvaluateGoals(eval) => this.format_added_goals_evaluation(eval)?,
ProbeStep::NestedProbe(probe) => this.format_probe(probe)?,
ProbeStep::CommitIfOkStart => writeln!(this.f, "COMMIT_IF_OK START")?,
ProbeStep::CommitIfOkSuccess => writeln!(this.f, "COMMIT_IF_OK SUCCESS")?,
}
}
Ok(())
Expand Down
159 changes: 34 additions & 125 deletions compiler/rustc_trait_selection/src/solve/alias_relate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//! Doing this via a separate goal is called "deferred alias relation" and part
//! of our more general approach to "lazy normalization".
//!
//! This is done by first normalizing both sides of the goal, ending up in
//! either a concrete type, rigid alias, or an infer variable.
//! This is done by first structurally normalizing both sides of the goal, ending
//! up in either a concrete type, rigid alias, or an infer variable.
//! These are related further according to the rules below:
//!
//! (1.) If we end up with two rigid aliases, then we relate them structurally.
Expand All @@ -14,18 +14,10 @@
//!
//! (3.) Otherwise, if we end with two rigid (non-projection) or infer types,
//! relate them structurally.
//!
//! Subtle: when relating an opaque to another type, we emit a
//! `NormalizesTo(opaque, ?fresh_var)` goal when trying to normalize the opaque.
//! This nested goal starts out as ambiguous and does not actually define the opaque.
//! However, if `?fresh_var` ends up geteting equated to another type, we retry the
//! `NormalizesTo` goal, at which point the opaque is actually defined.

use super::EvalCtxt;
use rustc_infer::traits::query::NoSolution;
use rustc_infer::traits::solve::GoalSource;
use rustc_middle::traits::solve::{Certainty, Goal, QueryResult};
use rustc_middle::ty::{self, Ty};
use rustc_middle::ty;

impl<'tcx> EvalCtxt<'_, 'tcx> {
#[instrument(level = "debug", skip(self), ret)]
Expand All @@ -36,141 +28,58 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
let tcx = self.tcx();
let Goal { param_env, predicate: (lhs, rhs, direction) } = goal;

let Some(lhs) = self.try_normalize_term(param_env, lhs)? else {
return self
.evaluate_added_goals_and_make_canonical_response(Certainty::overflow(true));
// Structurally normalize the lhs.
let lhs = if let Some(alias) = lhs.to_alias_ty(self.tcx()) {
let term = self.next_term_infer_of_kind(lhs);
self.add_normalizes_to_goal(goal.with(tcx, ty::NormalizesTo { alias, term }));
term
} else {
lhs
};

let Some(rhs) = self.try_normalize_term(param_env, rhs)? else {
return self
.evaluate_added_goals_and_make_canonical_response(Certainty::overflow(true));
// Structurally normalize the rhs.
let rhs = if let Some(alias) = rhs.to_alias_ty(self.tcx()) {
let term = self.next_term_infer_of_kind(rhs);
self.add_normalizes_to_goal(goal.with(tcx, ty::NormalizesTo { alias, term }));
term
} else {
rhs
};

// Apply the constraints.
self.try_evaluate_added_goals()?;
let lhs = self.resolve_vars_if_possible(lhs);
let rhs = self.resolve_vars_if_possible(rhs);
debug!(?lhs, ?rhs);

let variance = match direction {
ty::AliasRelationDirection::Equate => ty::Variance::Invariant,
ty::AliasRelationDirection::Subtype => ty::Variance::Covariant,
};

match (lhs.to_alias_ty(tcx), rhs.to_alias_ty(tcx)) {
(None, None) => {
self.relate(param_env, lhs, variance, rhs)?;
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}

(Some(alias), None) => {
self.relate_rigid_alias_non_alias(param_env, alias, variance, rhs)
self.relate_rigid_alias_non_alias(param_env, alias, variance, rhs)?;
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
(None, Some(alias)) => {
self.relate_rigid_alias_non_alias(
param_env,
alias,
variance.xform(ty::Variance::Contravariant),
lhs,
)?;
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
(None, Some(alias)) => self.relate_rigid_alias_non_alias(
param_env,
alias,
variance.xform(ty::Variance::Contravariant),
lhs,
),

(Some(alias_lhs), Some(alias_rhs)) => {
self.relate(param_env, alias_lhs, variance, alias_rhs)?;
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
}
}

/// Relate a rigid alias with another type. This is the same as
/// an ordinary relate except that we treat the outer most alias
/// constructor as rigid.
#[instrument(level = "debug", skip(self, param_env), ret)]
fn relate_rigid_alias_non_alias(
&mut self,
param_env: ty::ParamEnv<'tcx>,
alias: ty::AliasTy<'tcx>,
variance: ty::Variance,
term: ty::Term<'tcx>,
) -> QueryResult<'tcx> {
// NOTE: this check is purely an optimization, the structural eq would
// always fail if the term is not an inference variable.
if term.is_infer() {
let tcx = self.tcx();
// We need to relate `alias` to `term` treating only the outermost
// constructor as rigid, relating any contained generic arguments as
// normal. We do this by first structurally equating the `term`
// with the alias constructor instantiated with unconstrained infer vars,
// and then relate this with the whole `alias`.
//
// Alternatively we could modify `Equate` for this case by adding another
// variant to `StructurallyRelateAliases`.
let identity_args = self.fresh_args_for_item(alias.def_id);
let rigid_ctor = ty::AliasTy::new(tcx, alias.def_id, identity_args);
self.eq_structurally_relating_aliases(param_env, term, rigid_ctor.to_ty(tcx).into())?;
self.eq(param_env, alias, rigid_ctor)?;
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
} else {
Err(NoSolution)
}
}

// FIXME: This needs a name that reflects that it's okay to bottom-out with an inference var.
/// Normalize the `term` to equate it later.
#[instrument(level = "debug", skip(self, param_env), ret)]
fn try_normalize_term(
&mut self,
param_env: ty::ParamEnv<'tcx>,
term: ty::Term<'tcx>,
) -> Result<Option<ty::Term<'tcx>>, NoSolution> {
match term.unpack() {
ty::TermKind::Ty(ty) => {
Ok(self.try_normalize_ty_recur(param_env, 0, ty).map(Into::into))
}
ty::TermKind::Const(_) => {
if let Some(alias) = term.to_alias_ty(self.tcx()) {
let term = self.next_term_infer_of_kind(term);
self.add_normalizes_to_goal(Goal::new(
self.tcx(),
param_env,
ty::NormalizesTo { alias, term },
));
self.try_evaluate_added_goals()?;
Ok(Some(self.resolve_vars_if_possible(term)))
} else {
Ok(Some(term))
}
}
}
}

#[instrument(level = "debug", skip(self, param_env), ret)]
fn try_normalize_ty_recur(
&mut self,
param_env: ty::ParamEnv<'tcx>,
depth: usize,
ty: Ty<'tcx>,
) -> Option<Ty<'tcx>> {
if !self.tcx().recursion_limit().value_within_limit(depth) {
return None;
}

let ty::Alias(kind, alias) = *ty.kind() else {
return Some(ty);
};

match self.commit_if_ok(|this| {
let tcx = this.tcx();
let normalized_ty = this.next_ty_infer();
let normalizes_to = ty::NormalizesTo { alias, term: normalized_ty.into() };
match kind {
ty::AliasKind::Opaque => {
// HACK: Unlike for associated types, `normalizes-to` for opaques
// is currently not treated as a function. We do not erase the
// expected term.
this.add_goal(GoalSource::Misc, Goal::new(tcx, param_env, normalizes_to));
}
ty::AliasKind::Projection | ty::AliasKind::Inherent | ty::AliasKind::Weak => {
this.add_normalizes_to_goal(Goal::new(tcx, param_env, normalizes_to))
}
}
this.try_evaluate_added_goals()?;
Ok(this.resolve_vars_if_possible(normalized_ty))
}) {
Ok(ty) => self.try_normalize_ty_recur(param_env, depth + 1, ty),
Err(NoSolution) => Some(ty),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
/// whether an alias is rigid by using the trait solver. When instantiating a response
/// from the solver we assume that the solver correctly handled aliases and therefore
/// always relate them structurally here.
#[instrument(level = "debug", skip(infcx), ret)]
#[instrument(level = "debug", skip(infcx))]
fn unify_query_var_values(
infcx: &InferCtxt<'tcx>,
param_env: ty::ParamEnv<'tcx>,
Expand Down
47 changes: 0 additions & 47 deletions compiler/rustc_trait_selection/src/solve/eval_ctxt/commit_if_ok.rs

This file was deleted.

0 comments on commit fcb0e9d

Please sign in to comment.