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

Recursion limit during monomorphization hit quicker on beta #38033

Closed
alexcrichton opened this issue Nov 27, 2016 · 15 comments · Fixed by #38059
Closed

Recursion limit during monomorphization hit quicker on beta #38033

alexcrichton opened this issue Nov 27, 2016 · 15 comments · Fixed by #38059
Labels
A-trait-system Area: Trait system regression-from-stable-to-beta Performance or correctness regression from stable to beta. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@alexcrichton
Copy link
Member

This program compiles successfully on stable but hits the recursion limit on beta/nightly. This seems like a regression where we hit it too quickly? Is it something we can perhaps fix?

The specific error is:

$ rustc +beta foo.rs
error: reached the recursion limit during monomorphization (selection ambiguity)

cc @rust-lang/compiler
originally reported as rust-lang/futures-rs#262

@alexcrichton alexcrichton added regression-from-stable-to-beta Performance or correctness regression from stable to beta. I-nominated T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Nov 27, 2016
@alexcrichton
Copy link
Member Author

Inlining for posterity:

use std::marker;
use std::mem;

fn main() {
    let workers = (0..0).map(|_| result::<u32, ()>());
    drop(join_all(workers).poll());
}

trait Future {
    type Item;
    type Error;

    fn poll(&mut self) -> Result<Self::Item, Self::Error>;
}

trait IntoFuture {
    type Future: Future<Item=Self::Item, Error=Self::Error>;
    type Item;
    type Error;

    fn into_future(self) -> Self::Future;
}

impl<F: Future> IntoFuture for F {
    type Future = F;
    type Item = F::Item;
    type Error = F::Error;

    fn into_future(self) -> F {
        self
    }
}

struct FutureResult<T, E> {
    _inner: marker::PhantomData<(T, E)>,
}

fn result<T, E>() -> FutureResult<T, E> {
    loop {}
}

impl<T, E> Future for FutureResult<T, E> {
    type Item = T;
    type Error = E;

    fn poll(&mut self) -> Result<T, E> {
        loop {}
    }
}

struct JoinAll<I>
    where I: IntoIterator,
          I::Item: IntoFuture,
{
    elems: Vec<<I::Item as IntoFuture>::Item>,
}

fn join_all<I>(_: I) -> JoinAll<I>
    where I: IntoIterator,
          I::Item: IntoFuture,
{
    loop {}
}

impl<I> Future for JoinAll<I>
    where I: IntoIterator,
          I::Item: IntoFuture,
{
    type Item = Vec<<I::Item as IntoFuture>::Item>;
    type Error = <I::Item as IntoFuture>::Error;

    fn poll(&mut self) -> Result<Self::Item, Self::Error> {
        let elems = mem::replace(&mut self.elems, Vec::new());
        elems.into_iter().map(|e| {
            e
        }).collect::<Vec<_>>();
        loop {}
    }
}

@arielb1
Copy link
Contributor

arielb1 commented Nov 27, 2016

Thats... not a recursion limit error

                    debug!("Encountered ambiguity selecting `{:?}` during trans, \
                            presuming due to overflow",
                           trait_ref);
                    tcx.sess.span_fatal(span,
                        "reached the recursion limit during monomorphization \
                         (selection ambiguity)");

Overflow does not cause ambiguity for quite some time.

@eddyb
Copy link
Member

eddyb commented Nov 27, 2016

@arielb1 You mean this used to be the case, but now this is just ambiguous and not due to an overflow?

@arielb1
Copy link
Contributor

arielb1 commented Nov 27, 2016

DEBUG:rustc_trans::common: Encountered ambiguity selecting `Binder(<std::vec::Vec<u32> as std::vec::SpecExtend<std::iter::Map<std::vec::IntoIter<u32>, [[email protected]:74:31: 76:10]>>>)` during trans, presuming due to overflow

@arielb1
Copy link
Contributor

arielb1 commented Nov 27, 2016

There's no overflow, only ambiguity and a very outdated error message - overflow last caused ambiguity in the 0.X days.

@eddyb
Copy link
Member

eddyb commented Nov 27, 2016

Then this looks like it's caused by an interaction between a bug(?) in specialization and the new (in beta?) specialization of Extend for Vec.
cc @bluss Have you seen this fatal error before, when working on code using specialization?

@arielb1
Copy link
Contributor

arielb1 commented Nov 27, 2016

Sure. Trans-time ambiguity is always a bug.

@arielb1
Copy link
Contributor

arielb1 commented Nov 27, 2016

DEBUG:rustc::traits::select: assembled 2 candidates for TraitObligationStack(Obligation(predicate=Binder(TraitPredicate(<std::vec::Vec<u32> as std::vec::SpecExtend<std::iter::Map<std::vec::IntoIter<u32>, [[email protected]:74:31: 76:10]>>>)),depth=0)): [ImplCandidate(DefId { krate: CrateNum(3), node: DefIndex(4218) => collections/92fdee2a092512a3f348f1bfb6992470::vec[0]::{{impl}}[28] }), ImplCandidate(DefId { krate: CrateNum(3), node: DefIndex(4231) => collections/92fdee2a092512a3f348f1bfb6992470::vec[0]::{{impl}}[29] })]

Could we have a -Z non-verbose-def-ids mode for logs?

@eddyb
Copy link
Member

eddyb commented Nov 27, 2016

Oh I remember why this is so familiar: on one of my branches I broke a similar specialization by not sorting the predicates for constraining purposes. But here we can't just look at the history, this could be a bug that was always present in specialization, whether in this sorting logic or elsewhere.

@bluss
Copy link
Member

bluss commented Nov 27, 2016

@eddyb No, I have never seen it.

@arielb1 arielb1 added the A-trait-system Area: Trait system label Nov 27, 2016
@arielb1
Copy link
Contributor

arielb1 commented Nov 28, 2016

This is like an annoying problem with the projection cache's handling of nested obligations.

Nested projection obligations enter here in this case:

DEBUG:rustc::traits::project: AssociatedTypeNormalizer: depth=3 normalized 
<std::iter::Map<std::ops::Range<i32>, [[email protected]:5:30: 5:53]> as 
std::iter::IntoIterator>::Item to _#7t with 12 add'l obligations

Here the normalization result is the result of the nested impl <[[email protected]:5:30: 5:53] as FnMut(i32)>::Output, which is an additional obligation that is a part of "add'l obligations".

By itself, this is proper behaviour - the additional obligation is returned, and the RFC 447 rules ensure that it is processed before the output #_7t is used in any way.

However, the projection cache breaks this - it caches the <std::iter::Map<std::ops::Range<i32>,[[email protected]:5:30: 5:53]> as std::iter::IntoIterator>::Item = #_7t resolution. Now everybody else that attempts to look up the projection will just get #_7t without any additional obligations. This obviously causes all sorts of trouble (here a spurious EvaluatedToAmbig results in specializations not being discarded here).

This appears to work in most cases because during "one-pass evaluation" we tend to process obligations in LIFO order - after an obligation is added to the cache, we process its nested obligations before we do anything else (and if we have a cycle, we handle it specifically). And this specific case can be fixed by this little patch that removes the violation of LIFO order:

diff --git a/src/librustc/traits/project.rs b/src/librustc/traits/project.rs
index 76bead9..24731e8 100644
--- a/src/librustc/traits/project.rs
+++ b/src/librustc/traits/project.rs
@@ -1215,8 +1215,8 @@ fn confirm_closure_candidate<'cx, 'gcx, 'tcx>(
                                obligation,
                                &closure_type.sig,
                                util::TupleArgumentsFlag::No)
-        .with_addl_obligations(obligations)
         .with_addl_obligations(vtable.nested)
+        .with_addl_obligations(obligations)
 }
 
 fn confirm_callable_candidate<'cx, 'gcx, 'tcx>(

However, I am not sure relying on this "strict LIFO order evaluation" is particularly wise. cc @rust-lang/compiler .

@eddyb
Copy link
Member

eddyb commented Nov 28, 2016

@arielb1 I'd r+ that patch, backport it to beta, and separately decide on the underlying design problem.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Nov 28, 2016

@arielb1 I feel like I fixed this bug recently

UPDATE: Or perhaps a related one. I can't find the branch I was thinking of just now.

@nikomatsakis
Copy link
Contributor

Huh. Ah, I dimly recall now. I at one point had a fix that included the obligations in the cache entry, but I wasn't happy about it, and ultimately the problem was fixed by adding a call to with_addl_obligations(), probably the very same one you are talking about now. I think it's ok to rely on LIFO for the time being, though in general I hope we rework a lot of this code to be more general (I... "have thoughts").

@arielb1
Copy link
Contributor

arielb1 commented Nov 28, 2016

Opened PR.

arielb1 added a commit to arielb1/rust that referenced this issue Nov 28, 2016
This is an annoying gotcha with the projection cache's handling of
nested obligations.

Nested projection obligations enter the issue in this case:
```
DEBUG:rustc::traits::project: AssociatedTypeNormalizer: depth=3
normalized
<std::iter::Map<std::ops::Range<i32>,
[[email protected]:5:30: 5:53]> as
std::iter::IntoIterator>::Item to _#7t with 12 add'l obligations
```

Here the normalization result is the result of the nested impl
`<[[email protected]:5:30: 5:53] as FnMut(i32)>::Output`,
which is an additional obligation that is a part of "add'l obligations".

By itself, this is proper behaviour - the additional obligation is
returned, and the RFC 447 rules ensure that it is processed before the
output `#_7t` is used in any way.

However, the projection cache breaks this - it caches the
`<std::iter::Map<std::ops::Range<i32>,[[email protected]:5:30:
5:53]> as std::iter::IntoIterator>::Item = #_7t` resolution. Now
everybody else that attempts to look up the projection will just get
`#_7t` *without* any additional obligations. This obviously causes all
sorts of trouble (here a spurious `EvaluatedToAmbig` results in
specializations not being discarded
[here](https://github.com/rust-lang/rust/blob/9ca50bd4d50b55456e88a8c3ad8fcc9798f57522/src/librustc/traits/select.rs#L1705)).

The compiler works even with this projection cache gotcha because in most
cases during "one-pass evaluation". we tend to process obligations in LIFO
order - after an obligation is added to the cache, we process its nested
obligations before we do anything else (and if we have a cycle, we handle
it specifically) - which makes sure the inference variables are resolved
before they are used.

That "LIFO" order That was not done when projecting out of a closure, so
let's just fix that for the time being.

Fixes rust-lang#38033.
bors added a commit that referenced this issue Dec 3, 2016
evaluate obligations in LIFO order during closure projection

This is an annoying gotcha with the projection cache's handling of
nested obligations.

Nested projection obligations enter the issue in this case:
```
DEBUG:rustc::traits::project: AssociatedTypeNormalizer: depth=3
normalized
<std::iter::Map<std::ops::Range<i32>,
[[email protected]:5:30: 5:53]> as
std::iter::IntoIterator>::Item to _#7t with 12 add'l obligations
```

Here the normalization result is the result of the nested impl
`<[[email protected]:5:30: 5:53] as FnMut(i32)>::Output`,
which is an additional obligation that is a part of "add'l obligations".

By itself, this is proper behaviour - the additional obligation is
returned, and the RFC 447 rules ensure that it is processed before the
output `#_7t` is used in any way.

However, the projection cache breaks this - it caches the
`<std::iter::Map<std::ops::Range<i32>,[[email protected]:5:30:
5:53]> as std::iter::IntoIterator>::Item = #_7t` resolution. Now
everybody else that attempts to look up the projection will just get
`#_7t` *without* any additional obligations. This obviously causes all
sorts of trouble (here a spurious `EvaluatedToAmbig` results in
specializations not being discarded
[here](https://github.com/rust-lang/rust/blob/9ca50bd4d50b55456e88a8c3ad8fcc9798f57522/src/librustc/traits/select.rs#L1705)).

The compiler works even with this projection cache gotcha because in most
cases during "one-pass evaluation". we tend to process obligations in LIFO
order - after an obligation is added to the cache, we process its nested
obligations before we do anything else (and if we have a cycle, we handle
it specifically) - which makes sure the inference variables are resolved
before they are used.

That "LIFO" order That was not done when projecting out of a closure, so
let's just fix that for the time being.

Fixes #38033.

Beta-nominating because regression.

r? @nikomatsakis
brson pushed a commit to brson/rust that referenced this issue Dec 14, 2016
This is an annoying gotcha with the projection cache's handling of
nested obligations.

Nested projection obligations enter the issue in this case:
```
DEBUG:rustc::traits::project: AssociatedTypeNormalizer: depth=3
normalized
<std::iter::Map<std::ops::Range<i32>,
[[email protected]:5:30: 5:53]> as
std::iter::IntoIterator>::Item to _#7t with 12 add'l obligations
```

Here the normalization result is the result of the nested impl
`<[[email protected]:5:30: 5:53] as FnMut(i32)>::Output`,
which is an additional obligation that is a part of "add'l obligations".

By itself, this is proper behaviour - the additional obligation is
returned, and the RFC 447 rules ensure that it is processed before the
output `#_7t` is used in any way.

However, the projection cache breaks this - it caches the
`<std::iter::Map<std::ops::Range<i32>,[[email protected]:5:30:
5:53]> as std::iter::IntoIterator>::Item = #_7t` resolution. Now
everybody else that attempts to look up the projection will just get
`#_7t` *without* any additional obligations. This obviously causes all
sorts of trouble (here a spurious `EvaluatedToAmbig` results in
specializations not being discarded
[here](https://github.com/rust-lang/rust/blob/9ca50bd4d50b55456e88a8c3ad8fcc9798f57522/src/librustc/traits/select.rs#L1705)).

The compiler works even with this projection cache gotcha because in most
cases during "one-pass evaluation". we tend to process obligations in LIFO
order - after an obligation is added to the cache, we process its nested
obligations before we do anything else (and if we have a cycle, we handle
it specifically) - which makes sure the inference variables are resolved
before they are used.

That "LIFO" order That was not done when projecting out of a closure, so
let's just fix that for the time being.

Fixes rust-lang#38033.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-trait-system Area: Trait system regression-from-stable-to-beta Performance or correctness regression from stable to beta. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants