Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upImplement `impl Trait` in return type position by anonymization. #35091
Conversation
rust-highfive
assigned
nikomatsakis
Jul 28, 2016
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rust-highfive
Jul 28, 2016
Collaborator
(rust_highfive has picked a reviewer for you, use r? to override)
|
(rust_highfive has picked a reviewer for you, use r? to override) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Ryman
Jul 28, 2016
Contributor
$0 and $1 are resolved (to iter::Mapvec::Iter<T, closure> and the closure, respectively) and assigned to A and B, after checking the body of foo.
Will the #[must_use] attribute still cause a warning if the return value is unused in this case?
Will the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Jul 28, 2016
Member
@Ryman Not unless the type is normalized with Reveal::All in the lint, no.
|
@Ryman Not unless the type is normalized with |
eddyb
referenced this pull request
Jul 31, 2016
Open
Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071) #34511
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bors
Aug 1, 2016
Contributor
|
|
nikomatsakis
reviewed
Aug 5, 2016
| @@ -174,6 +174,7 @@ impl<'a, 'gcx, 'tcx> InferCtxt<'a, 'gcx, 'tcx> { | ||
| ty::TyEnum(..) | // OutlivesNominalType | ||
| ty::TyStruct(..) | // OutlivesNominalType | ||
| ty::TyBox(..) | // OutlivesNominalType (ish) | ||
| ty::TyAnon(..) | // OutlivesNominalType (ish) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 5, 2016
Contributor
This comment seems wrong. Perhaps "OutlivesProjectionComponents (ish)" -- really this merits a revising of the RFC 1214 rules to include OutlivesAnonType or something, but that's ok.
nikomatsakis
Aug 5, 2016
Contributor
This comment seems wrong. Perhaps "OutlivesProjectionComponents (ish)" -- really this merits a revising of the RFC 1214 rules to include OutlivesAnonType or something, but that's ok.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 5, 2016
Contributor
Probably this code is doing the right thing, but it's a bit more conservative than it might otherwise be. Really there are choices here just like for projections. Consider for example if you had trait Foo<'a,'b>: 'a, then you could in theory derive that impl Foo<'a,'b>: 'a even if 'b: 'a doesn't necessarily hold. Something similar could occur with trait Foo<'a, T>: 'a, where impl Foo<'a, &'b T>: 'a might hold even if 'b: 'a does not hold.
It seems in a way like an odd rule, since the type name that the type system gives includes lifetimes (like 'b) that are not necessarily in scope, but the same thing can occur with projections -- the point is that, after monomorphization, the normalized form of that type is known to outlive 'a, even if all components pre-normalization do not.
/me has that itchy feeling of wanting to revisit a richer, formalized version of this, to try and have a good reference. But nothing here makes me too scared yet. =)
nikomatsakis
Aug 5, 2016
Contributor
Probably this code is doing the right thing, but it's a bit more conservative than it might otherwise be. Really there are choices here just like for projections. Consider for example if you had trait Foo<'a,'b>: 'a, then you could in theory derive that impl Foo<'a,'b>: 'a even if 'b: 'a doesn't necessarily hold. Something similar could occur with trait Foo<'a, T>: 'a, where impl Foo<'a, &'b T>: 'a might hold even if 'b: 'a does not hold.
It seems in a way like an odd rule, since the type name that the type system gives includes lifetimes (like 'b) that are not necessarily in scope, but the same thing can occur with projections -- the point is that, after monomorphization, the normalized form of that type is known to outlive 'a, even if all components pre-normalization do not.
/me has that itchy feeling of wanting to revisit a richer, formalized version of this, to try and have a good reference. But nothing here makes me too scared yet. =)
nikomatsakis
reviewed
Aug 5, 2016
| @@ -907,6 +907,27 @@ impl<'tcx> fmt::Display for ty::TypeVariants<'tcx> { | ||
| } | ||
| TyTrait(ref data) => write!(f, "{}", data), | ||
| ty::TyProjection(ref data) => write!(f, "{}", data), | ||
| ty::TyAnon(def_id, substs) => { | ||
| ty::tls::with(|tcx| { | ||
| // Grab the "TraitA + TraitB" from `impl TraitA + TraitB`, |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 5, 2016
Contributor
As an aside, I suspect that the grammar ought to be impl (TraitA + TraitB) -- at least, the precedence of impl TraitA + TraitB looks surprising to me. Though maybe I could learn to like it.
I'm imagining things like F: Fn() -> impl Trait + Send <-- how should this be parsed?
nikomatsakis
Aug 5, 2016
Contributor
As an aside, I suspect that the grammar ought to be impl (TraitA + TraitB) -- at least, the precedence of impl TraitA + TraitB looks surprising to me. Though maybe I could learn to like it.
I'm imagining things like F: Fn() -> impl Trait + Send <-- how should this be parsed?
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Aug 5, 2016
Member
The +Trait is eagerly parsed, so F: Fn() -> (impl Trait + Send). However, impl Trait in a bound seems unlikely to me, and definitely not supported at this moment.
The precedence is this way for convenience - unlike trait objects, there's a prefix (impl), which solves all ambiguity save for what you just mentioned.
I suppose -> impl Fn() -> impl Trait + Send parses, as -> impl Fn() -> (impl Trait + Send).
eddyb
Aug 5, 2016
Member
The +Trait is eagerly parsed, so F: Fn() -> (impl Trait + Send). However, impl Trait in a bound seems unlikely to me, and definitely not supported at this moment.
The precedence is this way for convenience - unlike trait objects, there's a prefix (impl), which solves all ambiguity save for what you just mentioned.
I suppose -> impl Fn() -> impl Trait + Send parses, as -> impl Fn() -> (impl Trait + Send).
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 7, 2016
Contributor
The precedence is this way for convenience - unlike trait objects, there's a prefix (impl), which solves all ambiguity save for what you just mentioned.
I don't think there was an more-or-less "ambiguity" before, it's just that we had a weird set of precedence rules. I think the reason I find the precedence of impl relative to + surprising that, in general, we typically give keyword operators the same precedence as other unary operators and it usually higher than binary operators. Hence box 3 + 2 parses as (box 3) + 2. (Admittedly, though, I have complained about this in the past.)
In any case, I continue to find it inconsistent that the Send is associated with the trait in impl Trait + Send but with the type parameter F in F: Fn() Foo -> Trait + Send. (Even if I agree with your argument that one would not necessarily combine them... yet...)
Seems like maybe we should raise it on the tracking issue?
nikomatsakis
Aug 7, 2016
Contributor
The precedence is this way for convenience - unlike trait objects, there's a prefix (impl), which solves all ambiguity save for what you just mentioned.
I don't think there was an more-or-less "ambiguity" before, it's just that we had a weird set of precedence rules. I think the reason I find the precedence of impl relative to + surprising that, in general, we typically give keyword operators the same precedence as other unary operators and it usually higher than binary operators. Hence box 3 + 2 parses as (box 3) + 2. (Admittedly, though, I have complained about this in the past.)
In any case, I continue to find it inconsistent that the Send is associated with the trait in impl Trait + Send but with the type parameter F in F: Fn() Foo -> Trait + Send. (Even if I agree with your argument that one would not necessarily combine them... yet...)
Seems like maybe we should raise it on the tracking issue?
nikomatsakis
reviewed
Aug 5, 2016
| pub struct DeferredObligation<'tcx> { | ||
| pub predicate: ty::PolyTraitPredicate<'tcx>, | ||
| pub cause: ObligationCause<'tcx> | ||
| } |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 5, 2016
Contributor
I haven't read deeply here but I'm a bit dubious about the deferred obligations strategy. Even if we implement things this way, we may want to consider enforcing some kind of inference DAG on uses of impl Trait to give us room to consider later (I guess stabilization makes this less of a concern, actually, never mind).
Anyway, I'm thinking about situations like this one:
trait Foo {
type X;
}
impl<T> Foo for T {
default type X = ();
}
impl<T: Send> Foo for T {
type X = u32;
}Now if you have an obligation like <Y as Foo>::X = u32, where Y is an anonymous type, we have to figure out how to resolve it, and to do that we kind of need to know whether Y: Send.
In principle, we could probably defer both obligations (and maybe you do, as I said, I didn't finish reading this commit yet) but I do wonder if there will be other situations (method dispatch?) where it is very difficult to suspend.
nikomatsakis
Aug 5, 2016
Contributor
I haven't read deeply here but I'm a bit dubious about the deferred obligations strategy. Even if we implement things this way, we may want to consider enforcing some kind of inference DAG on uses of impl Trait to give us room to consider later (I guess stabilization makes this less of a concern, actually, never mind).
Anyway, I'm thinking about situations like this one:
trait Foo {
type X;
}
impl<T> Foo for T {
default type X = ();
}
impl<T: Send> Foo for T {
type X = u32;
}Now if you have an obligation like <Y as Foo>::X = u32, where Y is an anonymous type, we have to figure out how to resolve it, and to do that we kind of need to know whether Y: Send.
In principle, we could probably defer both obligations (and maybe you do, as I said, I didn't finish reading this commit yet) but I do wonder if there will be other situations (method dispatch?) where it is very difficult to suspend.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Aug 5, 2016
Member
AFAIK, even <() as Foo>::X = u32 won't type-check at this moment, so such speculations don't even come into play at this moment - although something like the lattice rule may add them.
eddyb
Aug 5, 2016
Member
AFAIK, even <() as Foo>::X = u32 won't type-check at this moment, so such speculations don't even come into play at this moment - although something like the lattice rule may add them.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 7, 2016
Contributor
AFAIK, even <() as Foo>::X = u32 won't type-check at this moment
Actually, it does: https://is.gd/36phAF
What does not work is something like normalizing <Rc<()> as Foo>::X to (), because we wanted to preserve the ability to specialize Foo for Rc in the future.
nikomatsakis
Aug 7, 2016
Contributor
AFAIK, even <() as Foo>::X = u32 won't type-check at this moment
Actually, it does: https://is.gd/36phAF
What does not work is something like normalizing <Rc<()> as Foo>::X to (), because we wanted to preserve the ability to specialize Foo for Rc in the future.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Aug 7, 2016
Member
Ah, right, I got it mixed up. Still, we don't pick the default, we just don't normalize, so doing the same for impl Trait would be just conservative, not wrong.
eddyb
Aug 7, 2016
Member
Ah, right, I got it mixed up. Still, we don't pick the default, we just don't normalize, so doing the same for impl Trait would be just conservative, not wrong.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 10, 2016
Contributor
doing the same for impl Trait would be just conservative, not wrong.
I think what you mean is: it won't be unsound? As in, we won't normalize to the wrong thing? If so, I agree.
nikomatsakis
Aug 10, 2016
Contributor
doing the same for impl Trait would be just conservative, not wrong.
I think what you mean is: it won't be unsound? As in, we won't normalize to the wrong thing? If so, I agree.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bors
Aug 6, 2016
Contributor
|
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
arielb1
Aug 7, 2016
Contributor
I don't like the "capture all lifetimes in the impl trait" approach and would prefer something more like lifetime elision.
|
I don't like the "capture all lifetimes in the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bors
Aug 9, 2016
Contributor
|
|
nikomatsakis
reviewed
Aug 10, 2016
src/librustc_typeck/rscope.rs
| /// scope, that anonymized types will close over. This property is | ||
| /// controlled by the region scope because it's fine-grained enough | ||
| /// to allow restriction of anonymized types to the syntactical extent | ||
| /// of a function's return type. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 10, 2016
Contributor
Nit: I can't make heads or tails of this comment :) An example would be great
nikomatsakis
Aug 10, 2016
Contributor
Nit: I can't make heads or tails of this comment :) An example would be great
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
eddyb
Aug 10, 2016
Member
Are you talking about allowing impl Trait, or the "generics in scope"? The latter would be easy to provide an example from (<'a, T> for fn foo<'a, T>(...) -> impl Trait), but controlling whether impl Trait is legal in a certain position is harder to explain.
For it to make more sense, rscope would have to become one of several properties that get propagated recursively.
At the same time, the ItemContext in typeck::collect may be a better choice now for holding the "generics in scope", as it's the only AstConv implementation which allows impl Trait, but it's still not that uniform (only the return type of a function or inherent impl method supports it, not just any type in the signature of those items).
eddyb
Aug 10, 2016
•
Member
Are you talking about allowing impl Trait, or the "generics in scope"? The latter would be easy to provide an example from (<'a, T> for fn foo<'a, T>(...) -> impl Trait), but controlling whether impl Trait is legal in a certain position is harder to explain.
For it to make more sense, rscope would have to become one of several properties that get propagated recursively.
At the same time, the ItemContext in typeck::collect may be a better choice now for holding the "generics in scope", as it's the only AstConv implementation which allows impl Trait, but it's still not that uniform (only the return type of a function or inherent impl method supports it, not just any type in the signature of those items).
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 10, 2016
Contributor
@eddyb after I read below I figured out what the comment meant. I think something like this would have helped me:
If this scope allows anonymized types, return the generics in scope that anonymized types will close over. For example, if you have a function like:
fn foo<'a, T>() -> impl Trait { ... }
then, for the scope that is used when handling the return type, this function would return 'a and T. This property is controlled by the region scope because it's fine-grained enough to allow restriction of anonymized types to the syntactical extentof a function's return type.
nikomatsakis
Aug 10, 2016
Contributor
@eddyb after I read below I figured out what the comment meant. I think something like this would have helped me:
If this scope allows anonymized types, return the generics in scope that anonymized types will close over. For example, if you have a function like:
fn foo<'a, T>() -> impl Trait { ... }
then, for the scope that is used when handling the return type, this function would return 'a and T. This property is controlled by the region scope because it's fine-grained enough to allow restriction of anonymized types to the syntactical extentof a function's return type.
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 10, 2016
Contributor
I went through the RFC and made a minimal list of test cases I think would like to see:
- positive tests for auto trait inference (something where we need to know that
impl Trait: Sendfor the code to work) - negative tests for auto trait inference (something where the code requires that
impl Trait: Sendbut in fact it does not) - test for interaction of auto trait inference and specialization (as mentioned in the comments)
- negative test where choice of regions is ambiguous, e.g., as described in this comment in the final example was one where the code ought to fail
- test where there IS a correct choice of regions (e.g. from that same comment); this could be positive or negative, depending on how the code currently behaves
- negative tests for attempts to use
impl Traitsyntax in places where it is not permitted:- arguments
- traits and impls of traits
- impl header
-
fn() -> impl FooorFn() -> impl Footypes - where clauses
- negative tests where we try to return values of two distinct types
- negative test for recursive fn that uses impl trait as in this example
- negative test that shows "true type" is not revealed to the caller (
let x: u32 = foo()) - positive test that shows that using specialization (or
sizeof) you can "reveal" the true type -- but only at trans time - negative test shows an attempt to "leak type" with specialization fails
- e.g.,
trait Foo { type T; } impl<T> Foo for T { default type T = (); } impl Foo for i32 { default type T = i32; }and then some code that would require<X as Foo>::T = i32whereXis an impl trait type
- e.g.,
- positive test for return type identity (two calls w/ same arguments yield "equal" types)
- negative test for return type identity (two calls w/ a different type argument yield "unequal" types)
- optional: test for region return type identity and region inference (because the two types must be the same, a loan is extended?)
- feature-gate testing (uses of impl trait where feature gate is NOT enabled)
|
I went through the RFC and made a minimal list of test cases I think would like to see:
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
nikomatsakis
Aug 10, 2016
Contributor
Er, I posted this comment earlier, but in the wrong place:
@eddyb ok so as discussed on IRC, I am in favor of landing this PR, on the assumption that we will address some of the thornier issues later.
One thing I think is missing is negative tests. I'd like to see at least some tests that if you try to return incompatible types that will result in an error. Probably some (more?) tests targeting "Auto trait" inference -- and the limits, e.g. around specialization -- are warranted as well.
One thing I have done in the past -- but didn't do yet for this PR -- is to make a list, based on the RFC, not the code, of things that might need to be tested, and then compare the code against that list. I'm happy to go through and do it, or you can and post it somewhere. :)
(In case it wasn't clear, the code itself seems good to me! Nice work as usual.)
|
Er, I posted this comment earlier, but in the wrong place: @eddyb ok so as discussed on IRC, I am in favor of landing this PR, on the assumption that we will address some of the thornier issues later. One thing I think is missing is negative tests. I'd like to see at least some tests that if you try to return incompatible types that will result in an error. Probably some (more?) tests targeting "Auto trait" inference -- and the limits, e.g. around specialization -- are warranted as well. One thing I have done in the past -- but didn't do yet for this PR -- is to make a list, based on the RFC, not the code, of things that might need to be tested, and then compare the code against that list. I'm happy to go through and do it, or you can and post it somewhere. :) (In case it wasn't clear, the code itself seems good to me! Nice work as usual.) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
@bors r+ |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
|
eddyb
referenced this pull request
Aug 11, 2016
Merged
Remove the ParamSpace separation from formal and actual generics in rustc. #35605
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bors
Aug 12, 2016
Contributor
|
|
eddyb
added some commits
Jun 30, 2016
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
@bors r=nikomatsakis |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
added a commit
that referenced
this pull request
Aug 12, 2016
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
bors
Aug 12, 2016
Contributor
Approved by: nikomatsakis
Pushing f55ac69 to master...
eddyb commentedJul 28, 2016
•
edited
Edited 1 time
-
eddyb
edited Jul 30, 2016 (most recent)
This is the first step towards implementing
impl Trait(cc #34511).impl Traittypes are only allowed in function and inherent method return types, and capture all named lifetime and type parameters, being invariant over them.No lifetimes that are not explicitly named lifetime parameters are allowed to escape from the function body.
The exposed traits are only those listed explicitly, i.e.
FooandCloneinimpl Foo + Clone, with the exception of "auto traits" (likeSendorSync) which "leak" the actual contents.The implementation strategy is anonymization, i.e.:
$0and$1are resolved (toiter::Map<vec::Iter<T>, closure>and the closure, respectively) and assigned toAandB, after checking the body offoo.AandBare never resolved for user-facing type equality (typeck), but always for the low-level representation and specialization (trans).The "auto traits" exception is implemented by collecting bounds like
impl Trait: Sendthat have failed for the obscureimpl Traittype (i.e.AorBabove), pretending they succeeded within the function and trying them again after type-checking the whole crate, by replacingimpl Traitwith the real type.While passing around values which have explicit lifetime parameters (of the function with
-> impl Trait) in their type should work, regionck appears to assign inference variables in way too many cases, and never properly resolving them to either explicit lifetime parameters, or'static.We might not be able to handle lifetime parameters in
impl Traitwithout changes to lifetime inference, but type parameters can have arbitrary lifetimes in them from the caller, so most type-generic usecases (or not generic at all) should not run into this problem.cc @rust-lang/lang