Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 20 additions & 20 deletions compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ where
goal_trait_ref: ty::TraitRef<I>,
impl_def_id: I::ImplId,
then: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResultOrRerunNonErased<I>,
) -> Result<Candidate<I>, NoSolutionOrRerunNonErased>;
) -> Result<Result<Candidate<I>, CandidateHeadUsages>, RerunNonErased>;

/// If the predicate contained an error, we want to avoid emitting unnecessary trait
/// errors but still want to emit errors for other trait goals. We have some special
Expand Down Expand Up @@ -457,17 +457,12 @@ impl AssembleCandidatesFrom {
}
}

/// This is currently used to track the [CandidateHeadUsages] of all failed `ParamEnv`
/// candidates. This is then used to ignore their head usages in case there's another
/// always applicable `ParamEnv` candidate. Look at how `param_env_head_usages` is
/// used in the code for more details.
///
/// We could easily extend this to also ignore head usages of other ignored candidates.
/// However, we currently don't have any tests where this matters and the complexity of
/// doing so does not feel worth it for now.
/// Head usages of failed candidates which may be ignored when their results
/// do not affect the selected candidate.
#[derive(Debug)]
pub(super) struct FailedCandidateInfo {

@lcnr lcnr Oct 29, 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.

I feel unsure here... I do somewhat dislike the current candidate assembly setup if we're tracking failed candidates anyways and need to explicitly track which candidates don't matter.

My current vague vibe is that it may actually be better to have a Candidates struct where we collect/discard candidates eagerly instead of collecting all of them and doing stuff at the end 🤔

in a way that'd be more similar to the existing behavior of

enum ProjectionCandidateSet<'tcx> {
None,
Single(ProjectionCandidate<'tcx>),
Ambiguous,
Error(SelectionError<'tcx>),
}
impl<'tcx> ProjectionCandidateSet<'tcx> {
fn mark_ambiguous(&mut self) {
*self = ProjectionCandidateSet::Ambiguous;
}
fn mark_error(&mut self, err: SelectionError<'tcx>) {
*self = ProjectionCandidateSet::Error(err);
}
// Returns true if the push was successful, or false if the candidate
// was discarded -- this could be because of ambiguity, or because
// a higher-priority candidate is already there.
fn push_candidate(&mut self, candidate: ProjectionCandidate<'tcx>) -> bool {
// This wacky variable is just used to try and
// make code readable and avoid confusing paths.
// It is assigned a "value" of `()` only on those
// paths in which we wish to convert `*self` to
// ambiguous (and return false, because the candidate
// was not used). On other paths, it is not assigned,
// and hence if those paths *could* reach the code that
// comes after the match, this fn would not compile.
let convert_to_ambiguous;
match self {
ProjectionCandidateSet::None => {
*self = ProjectionCandidateSet::Single(candidate);
return true;
}
ProjectionCandidateSet::Single(current) => {
// Duplicates can happen inside ParamEnv. In the case, we
// perform a lazy deduplication.
if current == &candidate {
return false;
}
// Prefer where-clauses. As in select, if there are multiple
// candidates, we prefer where-clause candidates over impls. This
// may seem a bit surprising, since impls are the source of
// "truth" in some sense, but in fact some of the impls that SEEM
// applicable are not, because of nested obligations. Where
// clauses are the safer choice. See the comment on
// `select::SelectionCandidate` and #21974 for more details.
match (current, candidate) {
(ProjectionCandidate::ParamEnv(..), ProjectionCandidate::ParamEnv(..)) => {
convert_to_ambiguous = ()
}
(ProjectionCandidate::ParamEnv(..), _) => return false,
(_, ProjectionCandidate::ParamEnv(..)) => bug!(
"should never prefer non-param-env candidates over param-env candidates"
),
(_, _) => convert_to_ambiguous = (),
}
}
ProjectionCandidateSet::Ambiguous | ProjectionCandidateSet::Error(..) => {
return false;
}
}
// We only ever get here when we moved from a single candidate
// to ambiguous.
let () = convert_to_ambiguous;
*self = ProjectionCandidateSet::Ambiguous;
false
}
}

🤔

View changes since the review

pub param_env_head_usages: CandidateHeadUsages,
pub impl_head_usages: CandidateHeadUsages,
}

impl<D, I> EvalCtxt<'_, D>
Expand All @@ -484,8 +479,10 @@ where
assemble_from: AssembleCandidatesFrom,
) -> Result<(Vec<Candidate<I>>, FailedCandidateInfo), RerunNonErased> {
let mut candidates = vec![];
let mut failed_candidate_info =
FailedCandidateInfo { param_env_head_usages: CandidateHeadUsages::default() };
let mut failed_candidate_info = FailedCandidateInfo {
param_env_head_usages: CandidateHeadUsages::default(),
impl_head_usages: CandidateHeadUsages::default(),
};
let Ok(normalized_self_ty) =
self.structurally_normalize_ty(goal.param_env, goal.predicate.self_ty())
else {
Expand Down Expand Up @@ -546,7 +543,11 @@ where
}),
};
if assemble_impls {
self.assemble_impl_candidates(goal, &mut candidates)?;
self.assemble_impl_candidates(
goal,
&mut candidates,
&mut failed_candidate_info,
)?;
self.assemble_object_bound_candidates(goal, &mut candidates);
}
}
Expand Down Expand Up @@ -588,17 +589,18 @@ where
&mut self,
goal: Goal<I, G>,
candidates: &mut Vec<Candidate<I>>,
failed_candidate_info: &mut FailedCandidateInfo,
) -> Result<(), RerunNonErased> {
let cx = self.cx();
let goal_trait_ref = goal.predicate.trait_ref(cx);
cx.for_each_relevant_impl(goal_trait_ref, |impl_def_id| -> Result<_, _> {
match G::consider_impl_candidate(self, goal, goal_trait_ref, impl_def_id, |ecx| {
ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
})
.map_err_to_rerun()?
{
})? {
Ok(candidate) => candidates.push(candidate),
Err(NoSolution) => {}
Err(head_usages) => {
failed_candidate_info.impl_head_usages.merge_usages(head_usages);
}
}

Ok(())
Expand Down Expand Up @@ -1220,11 +1222,9 @@ where
// other inference variables.
Err(NoSolution.into())
}
})
.map_err_to_rerun()?
{
})? {
Ok(candidate) => candidates.push(candidate),
Err(NoSolution) => {}
Err(_head_usages) => {}
}

Ok(())
Expand Down
14 changes: 8 additions & 6 deletions compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use rustc_type_ir::fast_reject::DeepRejectCtxt;
use rustc_type_ir::inherent::*;
use rustc_type_ir::lang_items::SolverTraitLangItem;
use rustc_type_ir::search_graph::CandidateHeadUsages;
use rustc_type_ir::solve::inspect::ProbeKind;
use rustc_type_ir::solve::{
AliasBoundKind, NoSolutionOrRerunNonErased, QueryResultOrRerunNonErased, RerunNonErased,
Expand Down Expand Up @@ -139,33 +140,34 @@ where
goal_trait_ref: ty::TraitRef<I>,
impl_def_id: I::ImplId,
then: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResultOrRerunNonErased<I>,
) -> Result<Candidate<I>, NoSolutionOrRerunNonErased> {
) -> Result<Result<Candidate<I>, CandidateHeadUsages>, RerunNonErased> {
let cx = ecx.cx();

let impl_trait_ref = cx.impl_trait_ref(impl_def_id);
if !DeepRejectCtxt::relate_rigid_infer(ecx.cx())
.args_may_unify(goal_trait_ref.args, impl_trait_ref.skip_binder().args)
{
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

// For every `default impl`, there's always a non-default `impl` that will *also* apply.
// There's no reason to register a candidate for this impl, since it is *not* proof that
// the trait goal holds.
if cx.impl_is_default(impl_def_id) {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

match cx.impl_polarity(impl_def_id) {
ty::ImplPolarity::Negative => return Err(NoSolution.into()),
ty::ImplPolarity::Negative => return Ok(Err(CandidateHeadUsages::default())),
ty::ImplPolarity::Positive => (),
};

if !cx.impl_is_const(impl_def_id) {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id)).enter(|ecx| {
let probe = ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id));
probe.enter_with_failed_candidate_head_usages(|ecx| {
let impl_args = ecx.fresh_args_for_item(impl_def_id.into());
ecx.record_impl_args(impl_args);
let impl_trait_ref = impl_trait_ref.instantiate(cx, impl_args).skip_norm_wip();
Expand Down
15 changes: 14 additions & 1 deletion compiler/rustc_next_trait_solver/src/solve/eval_ctxt/probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::marker::PhantomData;

use rustc_type_ir::search_graph::CandidateHeadUsages;
use rustc_type_ir::solve::{
AccessedOpaques, CanonicalResponse, NoSolutionOrRerunNonErased, RerunResultExt,
AccessedOpaques, CanonicalResponse, NoSolutionOrRerunNonErased, RerunNonErased, RerunResultExt,
};
use rustc_type_ir::{InferCtxtLike, Interner};
use tracing::{instrument, warn};
Expand Down Expand Up @@ -134,6 +134,19 @@ where
let (result, head_usages) = self.cx.enter_single_candidate(f);
Ok(Candidate { source: self.source, result: result?, head_usages })
}

/// Keep track of head usages even if the candidate does not apply.
#[instrument(level = "debug", skip_all, fields(source = ?self.source))]
pub(in crate::solve) fn enter_with_failed_candidate_head_usages(
self,
f: impl FnOnce(&mut EvalCtxt<'_, D>) -> Result<CanonicalResponse<I>, NoSolutionOrRerunNonErased>,
) -> Result<Result<Candidate<I>, CandidateHeadUsages>, RerunNonErased> {
let (result, head_usages) = self.cx.enter_single_candidate(f);
Ok(match result.map_err_to_rerun()? {
Ok(result) => Ok(Candidate { source: self.source, result, head_usages }),
Err(NoSolution) => Err(head_usages),
})
}
}

impl<'a, D, I> EvalCtxt<'a, D, I>
Expand Down
12 changes: 7 additions & 5 deletions compiler/rustc_next_trait_solver/src/solve/normalizes_to.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::debug_assert_matches;
use rustc_type_ir::fast_reject::DeepRejectCtxt;
use rustc_type_ir::inherent::*;
use rustc_type_ir::lang_items::{SolverAdtLangItem, SolverProjectionLangItem, SolverTraitLangItem};
use rustc_type_ir::search_graph::CandidateHeadUsages;
use rustc_type_ir::solve::{
FetchEligibleAssocItemResponse, NoSolutionOrRerunNonErased, QueryResultOrRerunNonErased,
RerunNonErased, RerunReason, RerunResultExt,
Expand Down Expand Up @@ -256,32 +257,33 @@ where
goal_trait_ref: ty::TraitRef<I>,
impl_def_id: I::ImplId,
then: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResultOrRerunNonErased<I>,
) -> Result<Candidate<I>, NoSolutionOrRerunNonErased> {
) -> Result<Result<Candidate<I>, CandidateHeadUsages>, RerunNonErased> {
let cx = ecx.cx();

let alias_def_id = goal.predicate.alias.expect_projection_def_id();
let impl_trait_ref = cx.impl_trait_ref(impl_def_id);
if !DeepRejectCtxt::relate_rigid_infer(ecx.cx())
.args_may_unify(goal_trait_ref.args, impl_trait_ref.skip_binder().args)
{
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

// For every `default impl`, there's always a non-default `impl` that will *also* apply.
// There's no reason to register a candidate for this impl, since it is *not* proof that
// the trait goal holds.
if cx.impl_is_default(impl_def_id) {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

// We have to ignore negative impls when projecting.
let impl_polarity = cx.impl_polarity(impl_def_id);
match impl_polarity {
ty::ImplPolarity::Negative => return Err(NoSolution.into()),
ty::ImplPolarity::Negative => return Ok(Err(CandidateHeadUsages::default())),
ty::ImplPolarity::Positive => {}
};

ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id)).enter(|ecx| {
let probe = ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id));
probe.enter_with_failed_candidate_head_usages(|ecx| {
let impl_args = ecx.fresh_args_for_item(impl_def_id.into());
let impl_trait_ref = impl_trait_ref.instantiate(cx, impl_args).skip_norm_wip();

Expand Down
19 changes: 13 additions & 6 deletions compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use rustc_type_ir::data_structures::IndexSet;
use rustc_type_ir::fast_reject::DeepRejectCtxt;
use rustc_type_ir::inherent::*;
use rustc_type_ir::lang_items::SolverTraitLangItem;
use rustc_type_ir::search_graph::CandidateHeadUsages;
use rustc_type_ir::solve::{
AliasBoundKind, CandidatePreferenceMode, CanonicalResponse, MaybeInfo,
NoSolutionOrRerunNonErased, OpaqueTypesJank, QueryResultOrRerunNonErased, RerunNonErased,
Expand Down Expand Up @@ -63,21 +64,21 @@ where
goal_trait_ref: TraitRef<I>,
impl_def_id: I::ImplId,
then: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResultOrRerunNonErased<I>,
) -> Result<Candidate<I>, NoSolutionOrRerunNonErased> {
) -> Result<Result<Candidate<I>, CandidateHeadUsages>, RerunNonErased> {
let cx = ecx.cx();

let impl_trait_ref = cx.impl_trait_ref(impl_def_id);
if !DeepRejectCtxt::relate_rigid_infer(ecx.cx())
.args_may_unify(goal_trait_ref.args, impl_trait_ref.skip_binder().args)
{
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

// For every `default impl`, there's always a non-default `impl` that will *also* apply.
// There's no reason to register a candidate for this impl, since it is *not* proof that
// the trait goal holds.
if cx.impl_is_default(impl_def_id) {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

match (cx.impl_polarity(impl_def_id), goal.predicate.polarity) {
Expand All @@ -88,15 +89,16 @@ where
// Impl doesn't match polarity
(ty::ImplPolarity::Positive, ty::ClausePolarity::Negative)
| (ty::ImplPolarity::Negative, ty::ClausePolarity::Positive) => {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}
}

if ecx.typing_mode().is_reflection() && !cx.is_fully_generic_for_reflection(impl_def_id) {
return Err(NoSolution.into());
return Ok(Err(CandidateHeadUsages::default()));
}

ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id)).enter(|ecx| {
let probe = ecx.probe_trait_candidate(CandidateSource::Impl(impl_def_id));
probe.enter_with_failed_candidate_head_usages(|ecx| {
let impl_args = ecx.fresh_args_for_item(impl_def_id.into());
ecx.record_impl_args(impl_args);
let impl_trait_ref = impl_trait_ref.instantiate(cx, impl_args).skip_norm_wip();
Expand Down Expand Up @@ -1548,6 +1550,11 @@ where
let where_bounds: Vec<_> = candidates
.extract_if(.., |c| matches!(c.source, CandidateSource::ParamEnv(_)))
.collect();

for candidate in candidates {
self.ignore_candidate_head_usages(candidate.head_usages);
}
self.ignore_candidate_head_usages(failed_candidate_info.impl_head_usages);
let Some((response, info)) = self.try_merge_candidates(&where_bounds) else {
return Ok((self.bail_with_ambiguity(&where_bounds), None));
};
Expand Down
44 changes: 44 additions & 0 deletions tests/ui/traits/next-solver/discarded-impl-head-usages-failed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//@ compile-flags: -Znext-solver=globally
//@ check-pass

// Failed impl candidates can still contribute head usages, even when a
// non-global ParamEnv candidate is preferred.
use std::marker::PhantomData;

trait Dependencies<D> {}

struct Graph<M, const STATE: u8>(PhantomData<M>);

fn impls<T: Dependencies<D>, D>() {}

trait NotImplemented {}

macro_rules! example {
($(($($dep:ident => $next:literal),+)),+ $(,)?) => {
fn hang<M>()
where
Graph<M, 0>: Dependencies<()>,
$($(Graph<M, $next>: Dependencies<()>,)+)+
{
impls::<Graph<M, 0>, _>();
}

$(
impl<M, const STATE: u8, $($dep,)+> Dependencies<($($dep,)+)>
for Graph<M, STATE>
where
$(Graph<M, $next>: Dependencies<$dep>,)+
Graph<M, STATE>: NotImplemented,
{}
)+
};
}

example! {
(D1 => 21, D2 => 22),
(D1 => 31, D2 => 32, D3 => 33),
(D1 => 41, D2 => 42, D3 => 43, D4 => 44),
(D1 => 51, D2 => 52, D3 => 53, D4 => 54, D5 => 55),
}

fn main() {}
40 changes: 40 additions & 0 deletions tests/ui/traits/next-solver/discarded-impl-head-usages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//@ compile-flags: -Znext-solver=globally
//@ check-pass

// Regression test for a performance issue where impl candidates discarded by a
// non-global ParamEnv preference could still contribute head usages and cause
// unnecessary canonical cycle reruns.
trait Dependencies<D> {}

struct Graph<M, const STATE: u8>(std::marker::PhantomData<M>);

fn impls<T: Dependencies<D>, D>() {}

macro_rules! example {
($(($($dep:ident => $next:literal),+)),+ $(,)?) => {
fn hang<M>()
where
Graph<M, 0>: Dependencies<()>,
$($(Graph<M, $next>: Dependencies<()>,)+)+
{
impls::<Graph<M, 0>, _>();
}

$(
impl<M, const STATE: u8, $($dep,)+> Dependencies<($($dep,)+)>
for Graph<M, STATE>
where
$(Graph<M, $next>: Dependencies<$dep>,)+
{}
)+
};
}

example! {
(D1 => 21, D2 => 22),
(D1 => 31, D2 => 32, D3 => 33),
(D1 => 41, D2 => 42, D3 => 43, D4 => 44),
(D1 => 51, D2 => 52, D3 => 53, D4 => 54, D5 => 55),
}

fn main() {}
Loading