Skip to content

[nll] spurious outlives requirement in shred-0.7.0 #53121

Description

@nikomatsakis

The following snippet, extracted from the crate shred v0.7.0, fails to compile with NLL, but only when compiled with --crate-type lib:

// NB You must compile with `--crate-type lib` to see the failure!

#![feature(nll)]

pub trait Accessor: Sized {
}

pub enum AccessorCow<'a, 'b, T>
where
    AccessorTy<'a, T>: 'b,
    T: System<'a> + ?Sized,
'a: 'b,
{
    /// A reference to an accessor.
    Ref(&'b AccessorTy<'a, T>),
    /// An owned accessor.
    Owned(AccessorTy<'a, T>),
}

type AccessorTy<'a, T> = <<T as System<'a>>::SystemData as DynamicSystemData<'a>>::Accessor;

pub trait System<'a> {
    type SystemData: DynamicSystemData<'a>;
}

pub trait SystemData<'a> {
}

pub trait DynamicSystemData<'a> {
    /// The accessor of the `SystemData`, which specifies the read and write dependencies and does
    /// the fetching.
    type Accessor: Accessor;
}

fn main() { }

yields

> rustc +nightly ~/tmp/reduce.rs -Zborrowck=mir -Ztwo-phase-borrows --crate-type lib

error[E0309]: the parameter type `T` may not live long enough
  --> /home/nmatsakis/tmp/reduce.rs:13:5
   |
13 |     Ref(&'b AccessorTy<'a, T>),
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: consider adding an explicit lifetime bound `T: 'b`...

error[E0309]: the parameter type `T` may not live long enough
  --> /home/nmatsakis/tmp/reduce.rs:15:5
   |
15 |     Owned(AccessorTy<'a, T>),
   |     ^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: consider adding an explicit lifetime bound `T: 'b`...

I'm not entirely sure what is going on here, but it seems very fishy, particularly since it is specific to --crate-type lib.

Activity

  1. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    NLL-completeWorking towards the "valid code works" goal
    on Aug 6, 2018
  2. added this to the Rust 2018 RC milestone on Aug 6, 2018
  3. changed the title [-][nll] [/-] [+][nll] spurious outlives requirement in shred-0.7.0[/+] on Aug 6, 2018
  4. nikomatsakis commented on Aug 6, 2018

    @nikomatsakis
    ContributorAuthor

    OK so I've dug into this a bit more. The NLL behavior here is buggy, I think, but I'm having trouble isolating it to a nicer test case because in general our behavior here is not that strong. I think that this struct ought to be well-formed as is -- requiring that T: 'b is sufficient but not necessary, the existing where AccessorTy<'a, T>: 'b ought to suffice.

    I think that the problem is that the NLL code is hitting this "fallback" in the code that handles obligations like AccessorTy<'0, T>: '1:

    // If declared bounds list is empty, the only applicable rule is
    // OutlivesProjectionComponent. If there are inference variables,
    // then, we can break down the outlives into more primitive
    // components without adding unnecessary edges.
    //
    // If there are *no* inference variables, however, we COULD do
    // this, but we choose not to, because the error messages are less
    // good. For example, a requirement like `T::Item: 'r` would be
    // translated to a requirement that `T: 'r`; when this is reported
    // to the user, it will thus say "T: 'r must hold so that T::Item:
    // 'r holds". But that makes it sound like the only way to fix
    // the problem is to add `T: 'r`, which isn't true. So, if there are no
    // inference variables, we use a verify constraint instead of adding
    // edges, which winds up enforcing the same condition.
    let needs_infer = projection_ty.needs_infer();
    if env_bounds.is_empty() && needs_infer {
    debug!("projection_must_outlive: no declared bounds");
    for component_ty in projection_ty.substs.types() {
    self.type_must_outlive(origin.clone(), component_ty, region);
    }
    for r in projection_ty.substs.regions() {
    self.delegate.push_sub_region_constraint(origin.clone(), region, r);
    }
    return;
    }

    In particular, if we encounter an obligation to prove AccessorTy<'0, T>: '1, we fallback to requiring that '0: '1 and T: '1. As the comment describes, this case is quite tricky because if we don't add constraints to inference, then '0 might be too small -- but if we do, we don't know which to add really.

    Anyway, since NLL uses region inference variables everywhere, it basically always hits this case, which makes us overly conservative in this example.

    I'm not sure yet the right fix. I suppose that in NLL we could avoid going down this path, but I think this will likely cause other failures.

  5. nikomatsakis commented on Aug 27, 2018

    @nikomatsakis
    ContributorAuthor

    I was thinking about this. I think maybe the best fix is that we should not be so eager to convert "outlives bounds" into type tests and constraints. We could hold off on that, do inference, and then do the type test conversion. This second round may of course add new constraints, and we can deal with that.

    This will be a bit of a pain, though, since we currently compute the SCCs etc based on the previous set of obligations, and this would introduce new edges into the graph. Not sure how to deal with that.

    Gah, still not entirely sure what to do here.

  6. pnkfelix commented on Sep 4, 2018

    @pnkfelix
    Contributor

    visited at meeting. assigning to niko to try to write up a plan of attack. (Also, we need to decide whether it is really an RC blocker or not)

  7. nikomatsakis commented on Sep 10, 2018

    @nikomatsakis
    ContributorAuthor

    Not an RC blocker: the migration strategy means this will be a warning anyway.

  8. nikomatsakis commented on Sep 11, 2018

    @nikomatsakis
    ContributorAuthor

    OK, so, for the record: the reason that this errors out only with --compile-type=lib is that the errors occur in the generated code for "variant constructors", and -- since the variants are unused, I suppose -- we only wind up generating that code when creating a library. You can get an error without --compile-type lib by using this gist, which coerces the variant into a function, thus forcing the code in question to be generated.

    I note though that we skip comparable struct constructors:

    if tcx.is_struct_constructor(def_id) {
    // We are not borrow checking the automatically generated struct constructors
    // because we want to accept structs such as this (taken from the `linked-hash-map`
    // crate):
    // ```rust
    // struct Qey<Q: ?Sized>(Q);
    // ```
    // MIR of this struct constructor looks something like this:
    // ```rust
    // fn Qey(_1: Q) -> Qey<Q>{
    // let mut _0: Qey<Q>; // return place
    //
    // bb0: {
    // (_0.0: Q) = move _1; // bb0[0]: scope 0 at src/main.rs:1:1: 1:26
    // return; // bb0[1]: scope 0 at src/main.rs:1:1: 1:26
    // }
    // }
    // ```
    // The problem here is that `(_0.0: Q) = move _1;` is valid only if `Q` is
    // of statically known size, which is not known to be true because of the
    // `Q: ?Sized` constraint. However, it is true because the constructor can be
    // called only when `Q` is of statically known size.
    return_early = true;
    }

    but anyway we should fix this bug. I believe that this example, from #53789, is the same underlying problem:

    #![feature(nll)]
    #![allow(unused_variables)]
    
    use std::collections::BTreeMap;
    
    trait ValueTree {
        type Value;
    }
    
    trait Strategy {
        type Value: ValueTree;
    }
    
    type StrategyFor<A> = StrategyType<'static, A>;
    type StrategyType<'a, A> = <A as Arbitrary<'a>>::Strategy;
    
    impl<K: ValueTree, V: ValueTree> Strategy for (K, V)
    {
        type Value = TupleValueTree<(K, V)>;
    }
    
    impl<K: ValueTree, V: ValueTree> ValueTree for TupleValueTree<(K, V)>
    {
        type Value = BTreeMapValueTree<K, V>;
    }
    
    struct TupleValueTree<T> {
        tree: T,
    }
    
    struct BTreeMapStrategy<K, V>(std::marker::PhantomData<(K, V)>)
    where
        K: Strategy,
        V: Strategy;
    
    struct BTreeMapValueTree<K, V>(
        std::marker::PhantomData<(K, V)>
    )
    where
        K: ValueTree,
        V: ValueTree;
    
    impl<K, V> Strategy for BTreeMapStrategy<K, V>
    where
        K: Strategy,
        V: Strategy,
    {
        type Value = BTreeMapValueTree<K::Value, V::Value>;
    }
    
    impl<K, V> ValueTree for BTreeMapValueTree<K, V>
    where
        K: ValueTree,
        V: ValueTree,
    {
        type Value = BTreeMap<K::Value, V::Value>;
    }
    
    trait Arbitrary<'a>: Sized {
        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy;
        type Parameters;
        type Strategy: Strategy<Value = Self::ValueTree>;
        type ValueTree: ValueTree<Value = Self>;
    }
    
    impl<'a, A, B> Arbitrary<'a> for BTreeMap<A, B>
    where
        A: Arbitrary<'static>,
        B: Arbitrary<'static>,
        StrategyFor<A>: 'static,
        StrategyFor<B>: 'static,
    {
        type ValueTree = <Self::Strategy as Strategy>::Value;
        type Parameters = (A::Parameters, B::Parameters);
        type Strategy = BTreeMapStrategy<A::Strategy, B::Strategy>;
        fn arbitrary_with(args: Self::Parameters) -> BTreeMapStrategy<A::Strategy, B::Strategy> {
            let (a, b) = args;
            btree_map(any_with::<A>(a), any_with::<B>(b))
        }
    }
    
    fn btree_map<K: Strategy + 'static, V: Strategy>(
        key: K,
        value: V
    ) -> BTreeMapStrategy<K, V>
    {
        unimplemented!()
    }
    
    fn any_with<'a, A: Arbitrary<'a>>(args: A::Parameters) -> StrategyType<'a, A> {
        unimplemented!()
    }
  9. nikomatsakis commented on Sep 11, 2018

    @nikomatsakis
    ContributorAuthor

    Minimized test showing the core problem:

    trait MyTrait<'a> {
        type Output;
    }
    
    fn foo<'a, T>() -> &'a ()
    where
        T: MyTrait<'a>, <T as MyTrait<'a>>::Output: 'a,
    {
        bar::<T::Output>()
    }
    
    fn bar<'a, T>() -> &'a ()
    where
        T: 'a,
    {
        &()
    }
    
    fn main() {}
  10. nikomatsakis commented on Sep 21, 2018

    @nikomatsakis
    ContributorAuthor

    Fix in #54453

  11. added a commit that references this issue on Sep 26, 2018
    c7df1f5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-NLLArea: Non-lexical lifetimes (NLL)NLL-completeWorking towards the "valid code works" goalT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions