Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Order-dependence of dyn Trait: Supertrait goals causes incompleteness (old solver) #123303

Closed
compiler-errors opened this issue Mar 31, 2024 · 7 comments · Fixed by #122791
Closed
Assignees
Labels
A-trait-objects Area: trait objects, vtable layout A-traits Area: Trait system C-bug Category: This is a bug. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@compiler-errors
Copy link
Member

pub trait Trait: Supertrait {}

trait Impossible {}
impl<F: ?Sized + Impossible> Trait for F {}

pub trait Supertrait {}

impl<T: ?Sized + Trait + Impossible> Supertrait for T {}

fn needs_supertrait<T: ?Sized + Supertrait>() {}
fn needs_trait<T: ?Sized + Trait>() {}

fn main() {
    needs_trait::<dyn Trait>(); // Comment this and it'll fail
    needs_supertrait::<dyn Trait>();
    needs_trait::<dyn Trait>();
}

Trying to prove dyn Trait: Trait only after we prove dyn Trait: Supertrait will cause the goal to fail. This seems bad.

@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Mar 31, 2024
@compiler-errors compiler-errors self-assigned this Mar 31, 2024
@compiler-errors
Copy link
Member Author

cc @lcnr who may know if this issue already is tracked

@fmease fmease added A-traits Area: Trait system T-types Relevant to the types team, which will review and decide on the PR/issue. A-trait-objects Area: trait objects, vtable layout C-bug Category: This is a bug. and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Mar 31, 2024
@compiler-errors
Copy link
Member Author

Actually this is just because EvaluatedToErrStackDependent is super scuffed. This will be fixed by #122791.

@compiler-errors
Copy link
Member Author

compiler-errors commented Mar 31, 2024

So what happens here:

  • We try to prove dyn Trait: Supertrait
    • Assemble impl candidate first, which requires dyn Trait: Trait and dyn Trait: Impossible
      • We try to prove dyn Trait: Trait
        • Assemble object-bound candidate, which requires dyn Trait: Supertrait to hold
          • We see this is a stack-dependent overflow, so it gets EvaluatedToErrStackDependent
        • Assemble impl candidate, which requires dyn Trait: impossible, so it gets EvaluatedToErr
        • During winnowing, neither candiate holds, so we filter both of them, and return SelectionError::Unimplemented THIS IS THE IMPORTANT PART
    • (at this point, we will assemble the object bound candidate, however we've already cached dyn Trait: Trait as ErrorGuaranteed, which causes the later goal to fail.)

Specifically, winnowing does not take into account stack dependence:

Ok(eval) if eval.may_apply() => {
Ok(Some(EvaluatedCandidate { candidate: c, evaluation: eval }))
}

And so therefore it may inadvertently "downgrade" a result that may be stack dependent to one that isn't stack dependent by removing it from the list of candidates, meaning we return SelectionError::Unimplemented (since we now have 0 candidates).

Unfortunately, this is only hit when we actually do winnowing -- if there's only one candidate, then we won't winnow:

// If there is more than one candidate, first winnow them down
// by considering extra conditions (nested obligations and so
// forth). We don't winnow if there is exactly one
// candidate. This is a relatively minor distinction but it
// can lead to better inference and error-reporting. An
// example would be if there was an impl:
//
// impl<T:Clone> Vec<T> { fn push_clone(...) { ... } }
//
// and we were to see some code `foo.push_clone()` where `boo`
// is a `Vec<Bar>` and `Bar` does not implement `Clone`. If
// we were to winnow, we'd wind up with zero candidates.
// Instead, we select the right impl now but report "`Bar` does
// not implement `Clone`".
if candidates.len() == 1 {
return self.filter_reservation_impls(candidates.pop().unwrap());
}

So that's why we don't see this more often.

@compiler-errors
Copy link
Member Author

If #122791 weren't so close to landing, then I would probably instead fix this by changing winnowing:

diff --git a/compiler/rustc_trait_selection/src/traits/select/mod.rs b/compiler/rustc_trait_selection/src/traits/select/mod.rs
index 1894fbba302..09d1df586c3 100644
--- a/compiler/rustc_trait_selection/src/traits/select/mod.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/mod.rs
@@ -492,10 +492,16 @@ fn candidate_from_obligation_no_cache<'o>(
         let mut candidates = candidates
             .into_iter()
             .map(|c| match self.evaluate_candidate(stack, &c) {
-                Ok(eval) if eval.may_apply() => {
+                // We CANNOT winnow `EvaluateToErrStackDependent`, since if we do,
+                // then we may end up with NO candidates in this list. If that's
+                // the case, then we will return `SelectionError::Unimplemented`,
+                // which results in `EvaluatedToErr` which is NOT stack dependent.
+                // This will cause us to incompletely cache a stack-dependent trait
+                // result as `EvaluatedToErr`.
+                Ok(EvaluatedToErr) => Ok(None),
+                Ok(eval) => {
                     Ok(Some(EvaluatedCandidate { candidate: c, evaluation: eval }))
                 }
-                Ok(_) => Ok(None),
                 Err(OverflowError::Canonical) => Err(Overflow(OverflowError::Canonical)),
                 Err(OverflowError::Error(e)) => Err(Overflow(OverflowError::Error(e))),
             })

@compiler-errors
Copy link
Member Author

This has probably failed since forever -- this example only fails since 1.49 which is when we started to enforce supertrait bounds hold (#73905) for object built-in candidates, but I wouldn't be surprised if you could construct an example which fails for even longer.

@lcnr
Copy link
Contributor

lcnr commented Apr 2, 2024

To clarify this a bit and to make sure I understand it correctly, the underlying issue here is the following afaict:

  • GoalA
    • CandA1
      • GoalB
        • CandB1
          • GoalA (inductive cycle -> error)
        • CandB1
          • Impossible (necessary to evaluate nested goals of CandB1 during selection)
    • CandA2 (trivially true)

when first evaluating GoalA, GoalB gets moved to the global cache as an error, evne though it only errors when used as a nested goal of GoalA. Later proving GoalB therefore fails.

@lcnr
Copy link
Contributor

lcnr commented Apr 2, 2024

I expect this to also affect EvaluatedToAmbigStackDependent with the following setup:

  • GoalA
    • CandA1
      • GoalB
        • CandB1
          • GoalA (inductive cycle -> ambig)
          • AssertInferenceConstraintsApplied (fails due to incompleteness if the inference constraints from CandA2 have not yet been applied)
        • CandB2
          • Impossible (necessary to evaluate nested goals of CandB1 during selection)
    • CandA2
      • guide inference ~> Ok, but inference constraints

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Apr 2, 2024
… r=estebank

Make sure to insert `Sized` bound first into clauses list

rust-lang#120323 made it so that we don't insert an implicit `Sized` bound whenever we see an *explicit* `Sized` bound. However, since the code that inserts implicit sized bounds puts the bound as the *first* in the list, that means that it had the **side-effect** of possibly meaning we check `Sized` *after* checking other trait bounds.

If those trait bounds result in ambiguity or overflow or something, it may change how we winnow candidates. (**edit: SEE** rust-lang#123303) This is likely the cause for the regression in rust-lang#123279 (comment), since the impl...

```rust
impl<T: Job + Sized> AsJob for T { // <----- changing this to `Sized + Job` or just `Job` (which turns into `Sized + Job`) will FIX the issue.
}
```

...looks incredibly suspicious.

Fixes [after beta-backport] rust-lang#123279.

Alternative is to revert rust-lang#120323. I don't have a strong opinion about this, but think it may be nice to keep the diagnostic changes around.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Apr 2, 2024
… r=estebank

Make sure to insert `Sized` bound first into clauses list

rust-lang#120323 made it so that we don't insert an implicit `Sized` bound whenever we see an *explicit* `Sized` bound. However, since the code that inserts implicit sized bounds puts the bound as the *first* in the list, that means that it had the **side-effect** of possibly meaning we check `Sized` *after* checking other trait bounds.

If those trait bounds result in ambiguity or overflow or something, it may change how we winnow candidates. (**edit: SEE** rust-lang#123303) This is likely the cause for the regression in rust-lang#123279 (comment), since the impl...

```rust
impl<T: Job + Sized> AsJob for T { // <----- changing this to `Sized + Job` or just `Job` (which turns into `Sized + Job`) will FIX the issue.
}
```

...looks incredibly suspicious.

Fixes [after beta-backport] rust-lang#123279.

Alternative is to revert rust-lang#120323. I don't have a strong opinion about this, but think it may be nice to keep the diagnostic changes around.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Apr 2, 2024
Rollup merge of rust-lang#123302 - compiler-errors:sized-bound-first, r=estebank

Make sure to insert `Sized` bound first into clauses list

rust-lang#120323 made it so that we don't insert an implicit `Sized` bound whenever we see an *explicit* `Sized` bound. However, since the code that inserts implicit sized bounds puts the bound as the *first* in the list, that means that it had the **side-effect** of possibly meaning we check `Sized` *after* checking other trait bounds.

If those trait bounds result in ambiguity or overflow or something, it may change how we winnow candidates. (**edit: SEE** rust-lang#123303) This is likely the cause for the regression in rust-lang#123279 (comment), since the impl...

```rust
impl<T: Job + Sized> AsJob for T { // <----- changing this to `Sized + Job` or just `Job` (which turns into `Sized + Job`) will FIX the issue.
}
```

...looks incredibly suspicious.

Fixes [after beta-backport] rust-lang#123279.

Alternative is to revert rust-lang#120323. I don't have a strong opinion about this, but think it may be nice to keep the diagnostic changes around.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-trait-objects Area: trait objects, vtable layout A-traits Area: Trait system C-bug Category: This is a bug. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants