Skip to content

-Znext-solver use the trait object's own bounds instead of goal when considering builtin object bounds#152859

Open
ShoyuVanilla wants to merge 2 commits intorust-lang:mainfrom
ShoyuVanilla:issue-152789
Open

-Znext-solver use the trait object's own bounds instead of goal when considering builtin object bounds#152859
ShoyuVanilla wants to merge 2 commits intorust-lang:mainfrom
ShoyuVanilla:issue-152789

Conversation

@ShoyuVanilla
Copy link
Member

@ShoyuVanilla ShoyuVanilla commented Feb 19, 2026

Fixes #152789 and fixes #151329

r? lcnr

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Feb 19, 2026
@ShoyuVanilla ShoyuVanilla force-pushed the issue-152789 branch 2 times, most recently from 4a25656 to 0f257f0 Compare February 19, 2026 16:57
@lcnr
Copy link
Contributor

lcnr commented Feb 27, 2026

hmm, thinking about this, given

pub trait Trait<T> {
    type Assoc;
}

pub trait Foo {
    type FooAssoc;
}

pub struct Wrap<U: Foo>(<dyn Trait<i32, Assoc = i64> as Trait<U::FooAssoc>>::Assoc)
where
    dyn Trait<i32, Assoc = i64>: Trait<U::FooAssoc>;

we're checking dyn Trait<i32, Assoc = i64>: Trait<U::FooAssoc> and try to do this via the builtin impl.

This builtin impl should be

impl Trait<i32> for dyn Trait<i32, Assoc = i64>
where
    i32: Sized,
    // we eagerly replace `<Self as Trait<i32>>::Assoc` with `i64` when
    // emitting the impl to avoid non-productive cycles.
    <Self as Trait<i32>>::Assoc: Sized,
{
    type Assoc = i64;
}

The builtin impl only applies here if U::FooAssoc is equal to i32. Generating the builtin impl should not actually care about the trait goal we're proving.

I think the bug is here

We should compute the where-clauses of the builtin impl using only the object type, not the goal we actually have to prove.

This builtin impl won't be applicable for U::FooAssoc because fn probe_and_match_goal_against_assumption will already reject the builtin candidate due to a type mismatch in the trait arguments.

@ShoyuVanilla ShoyuVanilla changed the title -Znext-solver Remove a problematic assertion from probing object bound candidates -Znext-solver se the trait object's own bounds instead of goal when considering builtin object bounds Feb 28, 2026
@ShoyuVanilla
Copy link
Member Author

That looks correct. I was being too narrow in my reasoning about the cause 😅

@rust-log-analyzer

This comment has been minimized.

@@ -83,14 +83,21 @@ where
assumption: I::Clause,
) -> Result<Candidate<I>, NoSolution> {
Self::probe_and_match_goal_against_assumption(ecx, source, goal, assumption, |ecx| {
ecx.try_evaluate_added_goals()?;
Copy link
Member Author

Choose a reason for hiding this comment

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

ecx.eq(goal.param_env, goal.predicate.trait_ref, assumption_trait_pred.trait_ref)?;
then(ecx)

We equate the goal with the assumption before entering this closure in the above lines.
However, that eq relationship does not actually hold in the problematic cases from the linked issues.

Because this additional goal is introduced through that equality, the ICE still occurs even if we use the trait ref from the trait object (instead of the goal's trait ref) when calling predicates_for_object_candidate.
Those predicates are evaluated here, in projection_may_match:

I think short-circuiting candidates for which the equality cannot hold, before entering the remaining probing, would be harmless and prevent the ICE.

In fact, adding just that early check is enough to fix the ICE in the linked issues.
But I think using trait object's own bounds instead of goal's is correct, so changed the follwing lines as well.

@@ -425,7 +425,7 @@ where
}
}
ty::Dynamic(tt, ..) => {
let principal = tt.principal().map(|p| p.def_id());
let principal = tt.principal_def_id();
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is irrelevant of the issue but seems trivial 😅

@ShoyuVanilla ShoyuVanilla changed the title -Znext-solver se the trait object's own bounds instead of goal when considering builtin object bounds -Znext-solver use the trait object's own bounds instead of goal when considering builtin object bounds Feb 28, 2026
@ShoyuVanilla
Copy link
Member Author

Hmm, the CI failure looks sus. Am I doing somewhat very incorrect thingy?

@lcnr
Copy link
Contributor

lcnr commented Mar 2, 2026

Because this additional goal is introduced through that equality, the ICE still occurs even if we use the trait ref from the trait object (instead of the goal's trait ref) when calling predicates_for_object_candidate.

How? do you still have the code which ICEd when using the trait object arguments lying around?

@ShoyuVanilla
Copy link
Member Author

How? do you still have the code which ICEd when using the trait object arguments lying around?

The very same code from the beginning

pub trait Trait<T> {
    type Assoc;
}

pub trait Foo {
    type FooAssoc;
}

pub struct Wrap<U: Foo>(<dyn Trait<i32, Assoc = i64> as Trait<U::FooAssoc>>::Assoc)
where
    dyn Trait<i32, Assoc = i64>: Trait<U::FooAssoc>;

fn main() {}

but maybe I've done what you said in a very wrong way, as the CI failed with very suspicious segfault: https://github.com/rust-lang/rust/actions/runs/22516543500/job/65235925254

@lcnr
Copy link
Contributor

lcnr commented Mar 2, 2026

but why:tm:

if you use the trait object itself to build the where-clauses, when do you encounter Trait<U::FooAssoc> while replacing stuff?

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Mar 2, 2026

It is encountered before replacing things.

fn probe_and_consider_object_bound_candidate(
ecx: &mut EvalCtxt<'_, D>,
source: CandidateSource<I>,
goal: Goal<I, Self>,
assumption: I::Clause,
) -> Result<Candidate<I>, NoSolution> {
Self::probe_and_match_goal_against_assumption(ecx, source, goal, assumption, |ecx| {
let cx = ecx.cx();
let ty::Dynamic(bounds, _) = goal.predicate.self_ty().kind() else {
panic!("expected object type in `probe_and_consider_object_bound_candidate`");
};
match structural_traits::predicates_for_object_candidate(
ecx,
goal.param_env,
goal.predicate.trait_ref(cx),
bounds,
) {

The replacement happens in L90, but we equate the goal with the assumption in L85 with probe_and_match_goal_against_assumption, before calling the closure. So, the nested goal added by such equating includes Trait<U::FooAssoc>.

fn projection_may_match(
&mut self,
source_projection: ty::Binder<I, ty::ProjectionPredicate<I>>,
target_projection: ty::AliasTerm<I>,
) -> bool {
source_projection.item_def_id() == target_projection.def_id
&& self
.ecx
.probe(|_| ProbeKind::ProjectionCompatibility)
.enter(|ecx| -> Result<_, NoSolution> {
let source_projection = ecx.instantiate_binder_with_infer(source_projection);
ecx.eq(self.param_env, source_projection.projection_term, target_projection)?;
ecx.try_evaluate_added_goals()
})
.is_ok()
}

We are currently(without this PR) adding such goals in L936, but even though we fix it by using trait object itself instead of goal, we are still adding it before replacing, so still errors on L937

@lcnr
Copy link
Contributor

lcnr commented Mar 2, 2026

but that's fine, is it not? the builtin impl should not apply here 🤔

we want to use the where-clause after all

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Mar 2, 2026

Yeah, such unprovable nested goal itself is fine and correct.

let mut matching_projections = replacements
.iter()
.filter(|source_projection| self.projection_may_match(**source_projection, alias_term));
let Some(replacement) = matching_projections.next() else {
// This shouldn't happen.
panic!("could not replace {alias_term:?} with term from from {:?}", self.self_ty);
};

But we still ICE because of L966 here, as projection_may_match filters out the correct(if we use the trait object itself) projection, because of that unprovable nested goal.

So, my intention of that line was to short circuit such unappliable builtin impl before trying replacements

@lcnr
Copy link
Contributor

lcnr commented Mar 2, 2026

ah, that's yeah... whether the projection matches shouldn't depend on goals from the parent. Eagerly checking whether the nested goals of equating the signature succeeds does fix this issue, but in theory, the try_evaluate_added_goals call there can make progress but not error, and if you call it we then get an error, so the ICE is still theoretically reachable 🤔

Given the way this is setup, all projections should structurally match and we shouldn't have to do any interesting type system stuff there at all. Could you replace the code to check whether the arguments are structurally equal instead of doing proper type system equality?

@ShoyuVanilla
Copy link
Member Author

Given the way this is setup, all projections should structurally match and we shouldn't have to do any interesting type system stuff there at all. Could you replace the code to check whether the arguments are structurally equal instead of doing proper type system equality?

Oh, that sounds like a correct way. Would it be sth like the following?

    fn projection_may_match(
        &mut self,
        source_projection: ty::Binder<I, ty::ProjectionPredicate<I>>,
        target_projection: ty::AliasTerm<I>,
    ) -> bool {
        source_projection.item_def_id() == target_projection.def_id
            && self
                .ecx
                .probe(|_| ProbeKind::ProjectionCompatibility)
                .enter(|ecx| -> Result<_, NoSolution> {
                    let source_projection = ecx.instantiate_binder_with_infer(source_projection);
-                   ecx.eq(self.param_env, source_projection.projection_term, target_projection)?;
-                   ecx.try_evaluate_added_goals()
+                   ecx.eq_structurally_relating_aliases(self.param_env, source_projection.projection_term, target_projection)
                })
                .is_ok()
    }

@lcnr
Copy link
Contributor

lcnr commented Mar 2, 2026

should be source_projection.no_bound_vars().unwrap() == target_projection (likely with a resolve_vars_if_possible call before then)

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Mar 3, 2026

Uh-oh, I encountered the very same problem expressed in this test:
https://github.com/rust-lang/rust/pull/139774/changes#diff-30c364a67db542a51da9d3cc9120515d449317be5a564efe64fe6ec01e9913d4
The test ICEs with exactly the same reason the test says and looks like structural equality isn't enough 🤔.

Would it make sense to do try_evaluate_added_goals above in a probe? It may still do not error out the when it should but it won't progress the context at least. It may error afterwards in projection_may_match together with the added eq relation between projections and then ICE again, but it would fix some cases, such as regression tests in this PR.

Or maybe I should devise more radical, correct fix. Maybe replacing projections before matching the goal against the assumption, or calling projection_match with a fresh new obligation ctxt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

-Znext-solver: trait object candidate ICE "could not replace AliasTerm" [ICE]: could not replace AliasTerm (unsatisifed bounds)

4 participants