- Feature Name: N/A
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#1214
- Rust Issue: rust-lang/rust#27579
Summary
Type system changes to address the outlives relation with respect to projections, and to better enforce that all types are well-formed (meaning that they respect their declared
- Simplify the outlives relation to be syntactically based.基となる、基底(の)
- Specify特定する、指定する、規定するimproved rules for the outlives relation and projections.
- Specify特定する、指定する、規定するmore specifically特にwhere WF bounds制限する、結び付けられてare enforced, covering several cases missing from the implementation.実装
The proposed changes here have been tested and found to cause only a modest number of regressions (about two dozen root regressions were previously found on crates.io; however, that run did not yet include all the provisions from this RFC; updated numbers coming soon). In order
- Initially, warnings will be issued for cases that violate違反するthe rules specified特定する、指定する、規定するin this RFC. These warnings are not lints and cannot be silenced except by correcting the code such that it type-checks under the new rules.
- After one release cycle, those warnings will become errors.
Note that although the changes do cause regressions, they also cause some code (like that in #23442) which currently gets errors to compile successfully.
Motivation
TL;DR
This is a long detailed RFC that is attempting to specify
Warnings first, errors later. Although the changes described
Associated type projections and lifetimes work more smoothly. The current rules for relating associated type projections (like T::Foo
) and lifetimes are somewhat cumbersome. The newer rules are more flexible, so that e.g. we can deduce that T::Foo: 'a
if T: 'a
, and similarlyT::Foo
is well-formed if T
is well-formed. As a bonus, the new rules are also sound. ;)
Simpler outlives relation. The older definitionT: 'a
was rather subtle. The new rule basically says that if all type/lifetime parametersT
must outlive 'a
, then T: 'a
(though there can also be other ways for us to decide that T: 'a
is valid,fn(&'x X): 'a
if 'x: 'a
and X: 'a
(presuming that X
is a type parameter). The older rules were based&'x X
is reachable
More types are sanity checked. Generally Rust requires that if you have a type like SomeStruct<T>
, then whatever where clausesSomeStruct
must holdT
(this is calledSomeStruct
is declared
then this impliesSomeStruct<f32>
is ill-formed, since f32
does not implementEq
(just PartialEq
). However, the current compiler doesn't check this in associated type definitions:
Similarly,fn(SomeStruct<f32>)
would be considered
There are a few other places where similar
To be well-formed, an Option<T>
type requires that T: Sized
. In this case, though T=Self
, and Self
is not Sized
by default. Therefore, this trait should be declaredtrait Foo: Sized
to be legal.
Impact on crates.io
This RFC has been largely implemented
Of the affected crates, 40 are receiving future compatibility
What follows
Problem | Future-compat. warnings | Errors |
---|---|---|
More types are sanity checked | 35 | 3 |
Simpler outlives relation | 5 |
As you can see, by far the largest source
Case | Problem | Number |
---|---|---|
1 | Self: Sized required | 26 |
2 | Foo: Bar required | 11 |
3 | Not object safe | 1 |
An example of each case follows:
Cases 1 and 2. In the compiler today, types appearingSelf
parameterOption<Self>
). Here is an example:
Because Option<T>
requires that T: Sized
, this trait should be declared
Case 2. Case 2 is the same as case 1, except that the missing boundSized
, or in some cases an outlives boundT: 'a
.
Case 3. The compiler currently permits
Projections and the outlives relation
RFC 192 introduced the outlives relation T: 'a
and described
the compiler is able to use implied
- all borrowed content in the type
I
outlives the function body; - all borrowed content in the type
I
outlives the lifetime'a
.
When associated types were introduced in RFC 195, some new rules were required to decide when an "outlives relation" involving a projection (e.g., I::Item: 'a
) should hold.I::Item
. Unfortunately, these adapted rules are not ideal, and can still lead to annoying errors in some situations. Finding a better solution has been on the agenda for some time.
Simultaneously, we realized in #24622 that the compiler had a bug that caused it to erroneouslyI::Item
outlived the current function body, just as it assumes that type parametersI
outlive the current function body. This bug can lead to unsound behavior.
This RFC describes
Well-formed types
A type is considered&'a T
or [T]
, these criteria are built into the language.
For example, consider
Here, the WF criteria for DeltaMap<K,V>
are as follows:
K: Hash
, because of the where-clause,K: 'a
, because of the where-clause,V: 'a
, because of the where-clauseK: Sized
, because of the implicit暗黙のSized
bound制限する、結び付けられてV: Sized
, because of the implicit暗黙のSized
bound制限する、結び付けられて
Let's look at those K:'a
boundsbase_map
has the type &'a mut HashMap<K,V>
, and this type is only validK: 'a
and V: 'a
hold.K
and V
are, we have to surface this requirement in the form
An aside: explicit明示的な WF requirements on types
You might wonder why you have to write K:Hash
and K:'a
explicitly.DeltaMap<'foo,T,U>
is well-formed without having to inspect the types of the fields -- that is, in the current design,DeltaMap<'foo,T,U>
is well-formed is the set
This has real consequencesK:Hash
or K:'a
, but the origin of the bound
Now, for Wrap1<'foo,T>
to be well-formed, T:'foo
and T:Hash
must hold,Wrap1
. Instead, you must trace deeply through its fields to find out that this obligation exists.
Implied暗黙の lifetime bounds制限する、結び付けられて
To help avoid
To see this in action, considerDeltaMap
:
You'll notice that there are no K:'a
or V:'a
annotations required here. This is due to implied
As the name "impliedK:'a
or V:'a
, but not K:Hash
-- this must still be written manually. It might be a good idea to change this, but that would be the topic of a separate
Currently, implied
NB. There is an additional
Missing WF checks
Unfortunately, the compiler currently fails to enforce WF in several important cases. For example, the following
However, if we simply naively add the requirement that associated types must be well-formed, this results
The problem here is that for &'a T
to be well-formed, T: 'a
must hold,
Detailed design設計(する)
This section
A little type grammar文法
We extend
T = scalar (i32, u32, ...) // Boring stuff
| X // Type variable
| Id<P0..Pn> // Nominal type (struct, enum)
| &r T // Reference (mut doesn't matter here)
| O0..On+r // Object type
| [T] // Slice type
| for<r..> fn(T1..Tn) -> T0 // Function pointer
| <P0 as Trait<P1..Pn>>::Id // Projection
P = r // Region name
| T // Type
O = for<r..> TraitId<P1..Pn> // Object type fragment
r = 'x // Region name
We'll use this to describe the rules in detail.
A quick note on terminology: an "object type fragment" is part of an object type: so if you have Box<FnMut()+Send>
, FnMut()
and Send
are object type fragments. Object type fragments are identicalP0
).
Syntactic definition定義 of the outlives relation
The outlives relation is definedR=<r0..rn>
. Initially, this setR
is empty,for<..>
.
Simple outlives rules
Here are the rules covering the simple cases, where no type parameters
OutlivesScalar:
--------------------------------------------------
R ⊢ scalar: 'a
OutlivesNominalType:
∀i. R ⊢ Pi: 'a
--------------------------------------------------
R ⊢ Id<P0..Pn>: 'a
OutlivesReference:
R ⊢ 'x: 'a
R ⊢ T: 'a
--------------------------------------------------
R ⊢ &'x T: 'a
OutlivesObject:
∀i. R ⊢ Oi: 'a
R ⊢ 'x: 'a
--------------------------------------------------
R ⊢ O0..On+'x: 'a
OutlivesFunction:
∀i. R,r.. ⊢ Ti: 'a
--------------------------------------------------
R ⊢ for<r..> fn(T1..Tn) -> T0: 'a
OutlivesFragment:
∀i. R,r.. ⊢ Pi: 'a
--------------------------------------------------
R ⊢ for<r..> TraitId<P0..Pn>: 'a
Outlives for lifetimes
The outlives relation for lifetimes depends on whether the lifetime in question was bound
OutlivesRegionEnv:
'x ∉ R // not a bound region
('x: 'a) in Env // derivable from where-clauses etc
--------------------------------------------------
R ⊢ 'x: 'a
OutlivesRegionReflexive:
--------------------------------------------------
R ⊢ 'a: 'a
OutlivesRegionTransitive:
R ⊢ 'a: 'c
R ⊢ 'c: 'b
--------------------------------------------------
R ⊢ 'a: 'b
For higher-ranked lifetimes, we simply ignorefor<'a> fn(&'a i32): 'x
holds,'a
is (and in fact it may be instantiated many times with different values on each call
OutlivesRegionBound:
'x ∈ R // bound region
--------------------------------------------------
R ⊢ 'x: 'a
Outlives for type parameters仮引数
For type parameters,
OutlivesTypeParameterEnv:
X: 'a in Env
--------------------------------------------------
R ⊢ X: 'a
Outlives for projections
Projections have the most possibilities. First, we may find information in the in-scope where clauses,type Foo: 'static
). These rule only apply
OutlivesProjectionEnv:
<P0 as Trait<P1..Pn>>::Id: 'b in Env
<> ⊢ 'b: 'a
--------------------------------------------------
<> ⊢ <P0 as Trait<P1..Pn>>::Id: 'a
OutlivesProjectionTraitDef:
WC = [Xi => Pi] WhereClauses(Trait)
<P0 as Trait<P1..Pn>>::Id: 'b in WC
<> ⊢ 'b: 'a
--------------------------------------------------
<> ⊢ <P0 as Trait<P1..Pn>>::Id: 'a
All the rules covered so far already exist today. This last rule, however, is not only new, it is the crucial insight of this RFC. It states that if all the components'a
, then the projection must outlive 'a
:
OutlivesProjectionComponents:
∀i. R ⊢ Pi: 'a
--------------------------------------------------
R ⊢ <P0 as Trait<P1..Pn>>::Id: 'a
Given
Let's begin with a concretestd::vec::Iter<'a,T>
. We are interested in the projection of Iterator::Item
:
<Iter<'a,T> as Iterator>::Item
or, in the more succint (but potentially ambiguous) form:
Iter<'a,T>::Item
Since I'm going to be talking a lot about this type, let's just call<PROJ>
for now. We would like to determine<PROJ>: 'x
holds.
Now, the easy way to solve <PROJ>: 'x
would be to normalize <PROJ>
by looking at the relevant impl:
From this impl, we can conclude that <PROJ> == &'a T
, and thus<PROJ>: 'x
to &'a T: 'x
, which in turn holds'a: 'x
and T: 'x
(from the rule OutlivesReference
).
But often we are in a situation where we can't normalize the projection (for example, a projection like I::Item
where we only know that I: Iterator
). What can we do then? The rule OutlivesProjectionComponents
says that if we can conclude that every lifetime/type parameterPi
to the trait reference'x
, then we know that a projection from those parameters'x
. In our example, the trait reference<Iter<'a,T> as Iterator>
, so that means that if the type Iter<'a,T>
outlives 'x
, then the projection <PROJ>
outlives 'x
. Now, you can see that this trivially reduces to the same resultIter<'a,T>: 'x
holds'a: 'x
and T: 'x
(from the rule OutlivesNominalType
).
OK, so we've seen that applyingOutlivesProjectionComponents
comes up with the same result
The basis of the rule comes from reasoning about the impl that we used to do normalization. Let's consider
So when we normalize <PROJ>
, we obtainΘ
to <TYPE>
. This substitution<PROJ> == Θ <Iter<'b,U> as Iterator>::Item
. In this case, that means Θ
would be ['b => 'a, U => T]
(and of course <TYPE>
would be &'b U
, but we're not supposed to rely on that).
The key idea for the OutlivesProjectionComponents
is that the only way that <TYPE>
can fail to outlive 'x
is if either:
- it names some lifetime parameter仮引数
'p
where'p: 'x
does not hold;保有する、有効であるor, - it names some type parameter仮引数
X
whereX: 'x
does not hold.保有する、有効である
Now, the only way that <TYPE>
can referP
is if it is brought in by the substitutionΘ
. So, if we can just show that all the types/lifetimes that in the rangeΘ
outlive 'x
, then we know that Θ <TYPE>
outlives 'x
.
Put yet another way: imagine that you have an impl with no parameters
Clearly, whatever <TYPE>
is, it can only refer'static
. So <Foo as Iterator>::Item: 'static
holds.<TYPE>
is -- we just need to see that the trait reference<Foo as Iterator>
doesn't have any lifetimes or type parameters
Implementation実装 complications
The current region inference code only permits
C = r0: r1
| C AND C
This is convenient because a simple fixed-point iteration
Unfortunately, this constraint<T as Trait<'X>>::Item: 'Y
, where 'X
and 'Y
are both region variables whose value is being inferred. At this point, there are several inference rules which could potentially apply.<T as Trait<'a>>::Item: 'b
. In that case, if 'X == 'a
and 'b: 'Y
, then we could employ the OutlivesProjectionEnv
rule. This would correspond
C = 'X:'a AND 'a:'X AND 'b:'Y
Otherwise,T: 'a
and 'X: 'Y
, then we could use the OutlivesProjectionComponents
rule, which would require a constraint
C = C1 AND 'X:'Y
where C1
is the constraintT:'a
.
As you can see, these two rules yieldedOR
constraint,
This complication is unfortunate, but to a large extent already exists with where-clauses and trait matching (see e.g. #21974). (Moreover, it seems to be inherent to the concept of assocated types, since they take
For the time being, the current implementationOutlivesProjectionComponents
rule. This is always sufficient but may be stricter than necessary. If there are no region variables in the projection, then it can simply run inference to completion and check each of the other two rules in turn. (It is still necessary to run inference because the bound
The WF relation
This section
The WF relation is really pretty simple: it just says that a type is "self-consistent". Typically,X
if you didn't declare
WfScalar:
--------------------------------------------------
R ⊢ scalar WF
WfParameter:
--------------------------------------------------
R ⊢ X WF // where X is a type parameter
WfTuple:
∀i. R ⊢ Ti WF
∀i<n. R ⊢ Ti: Sized // the *last* field may be unsized
--------------------------------------------------
R ⊢ (T0..Tn) WF
WfNominalType:
∀i. R ⊢ Pi Wf // parameters must be WF,
C = WhereClauses(Id) // and the conditions declared on Id must hold...
R ⊢ [P0..Pn] C // ...after substituting parameters, of course
--------------------------------------------------
R ⊢ Id<P0..Pn> WF
WfReference:
R ⊢ T WF // T must be WF
R ⊢ T: 'x // T must outlive 'x
--------------------------------------------------
R ⊢ &'x T WF
WfSlice:
R ⊢ T WF
R ⊢ T: Sized
--------------------------------------------------
[T] WF
WfProjection:
∀i. R ⊢ Pi WF // all components well-formed
R ⊢ <P0: Trait<P1..Pn>> // the projection itself is valid
--------------------------------------------------
R ⊢ <P0 as Trait<P1..Pn>>::Id WF
WF checking and higher-ranked types
There are two places in Rust where types can introduceR
of bound
WfFn:
∀i. R, r.. ⊢ Ti WF
--------------------------------------------------
R ⊢ for<r..> fn(T1..Tn) -> T0 WF
Basically, this rule adds the boundR
and then checks whether the argumentHashSet<K>
which requires that K: Hash
, then fn(HashSet<NoHash>)
would be illegalNoHash: Hash
does not hold,for<'a> fn(HashSet<&'a NoHash>)
would be legal,&'a NoHash: Hash
involves a bound'a
. See the "Checking Conditions"
Note that fn
types do not require that T0..Tn
be Sized
. This is intentional. The limitation that only sized values can be passed as argumentSized
(this will be checked in the implementations,Self
type parameterSized
by default (RFC 546), requiring that argumentSized
in trait definitions
The object type rule is similar,
WfObject:
rᵢ = union of implied region bounds from Oi
∀i. rᵢ: r
∀i. R ⊢ Oi WF
--------------------------------------------------
R ⊢ O0..On+r WF
The first two clausesr
must be an approximation for the the implicitrᵢ
derived from the trait definitions.
and a trait object like Foo+'x
, when we require that 'static:
(which is true, clearly, but in some cases the implicit'static
but rather some named lifetime).
The next clause
WfObjectFragment:
∀i. R, r.. ⊢ Pi
TraitId is object safe
--------------------------------------------------
R ⊢ for<r..> TraitId<P1..Pn>
Note that we don't check the where clausesSelf
type is not known (this is an object, after all), and hence we can't check them in general.
WF checking a trait reference参照
In some contexts, we want to check a trait reference,
WfTraitReference:
∀i. R, r.. ⊢ Pi
C = WhereClauses(Id) // and the conditions declared on Id must hold...
R, r0...rn ⊢ [P0..Pn] C // ...after substituting parameters, of course
--------------------------------------------------
R ⊢ for<r..> P0: TraitId<P1..Pn>
The rules are fairly straightforward. The components
Checking conditions条件
In variousR ̣⊢ WhereClause
. Here, R
representsWhereClause
does not use any of the regions in R
. In that case, we can ignoreWhereClause
holds.WhereClause
does referR
, then we simply considerR ⊢ WhereClause
to hold.
In practical terms,
and a function type like for<'a> fn(i: Iterator<'a, T>)
then this type is consideredT: 'a
holds.'a ⊢ T: 'a
.
However, if I have a type like
and a function type like for<'a> fn(f: Foo<'a, T>)
, I still must show that T: Eq
holds'a ⊢ T: Eq
, but 'a
is not referenced there.
Implied暗黙の bounds制限する、結び付けられて
ImpliedT
are givenT: WF
. Since we currently limit ourselves to implied
'a:'r
, where two regions must be related;X:'r
, where a type parameter仮引数X
outlives a region; or,<T as Trait<..>>::Id: 'r
, where a projection outlives a region.
Some caution is required around projections when deriving impliedX::Id: 'r
, we cannot for example deduce that X: 'r
must hold.X: 'r
is sufficient for X::Id: 'r
to hold,X::Id: 'r
to hold.X::Id: 'r
holds,X: 'r
.
When should we check the WF relation and under what conditions?
Currently the compiler performs WF checking in a somewhat haphazard way: in some cases (such as impls), it omits
Constants/statics. The type of a constant
Struct/enum declarations.
Function items. For function items, the environment
- argument引数types;
- return type;
- where clauses;句、節
- types of local variables.
These WF requirements are imposed at each fn or associated fn definition
Trait impls. In a trait impl, we assumeimpl<'a,T> SomeTrait for &'a T
, the environmentT: Sized
(explicit where-clause) and T: 'a
(implied bound&'a T
). This environment
- Where-clauses declared宣言on the trait must be WF.
- Associated types must be WF in the trait environment.環境
- The types of associated constants定数must be WF in the trait environment.環境
- Associated fns are checked just like regular普通の、正規のfunction items, but with the additional追加のimplied暗黙のbounds制限する、結び付けられてfrom the impl signature.シグネチャ
Inherent impls. In an inherent impl, we can assume
Trait declarations.
Type aliases.
Several points in the list
Fns being called.
Method calls,
Normalizing associated type references.T::Foo
is normalized, we will require that the trait reference
Drawbacks
N/A
Alternatives代わりのもの、選択肢
I'm not aware of any appealing alternatives.
Unresolved questions
Best policy for type aliases.type IteratorAndItem<T:Iterator> = (T::Item, T)
). Still, full-checking of WF on type aliases
For trait object type fragments, should we check WF conditions
should an object like Box<HashSet<NotHash>>
be illegal? It seems like that would be inline with our "best effort" approach to bound
Appendix
The informal explanation glossed over some details. This appendix tries to be a bit more thorough with how it is that we can conclude that a projection outlives 'a
if its inputs outlive 'a
. To start, let's specify<PROJ>
as:
<P0 as Trait<P1...Pn>>::Id
where P
can be a lifetime or type parameter
Then we know that there exists some impl of the form:
Here again, X
can be a lifetime or type parameterQ
can be any lifetime or type parameter.
Let Θ
be a suitable substitution[Xi => Ri]
such that ∀i. Θ Qi == Pi
(in other words, so that the impl applies<PROJ>
is Θ T
. Note that because trait matching is invariant, the types must be exactly
RFC 447 and #24461 require that a parameterXi
can only appearT
if it is constrained by the trait reference<Q0 as Trait<Q1..Qn>>
. The full definitionXi
appearsQ0..Qn
somewhere outside of a projection. Let's callConstrained(Q0..Qn)
.
Recall the rule OutlivesProjectionComponents
:
OutlivesProjectionComponents:
∀i. R ⊢ Pi: 'a
--------------------------------------------------
R ⊢ <P0 as Trait<P1..Pn>>::Id: 'a
We aim to show that ∀i. R ⊢ Pi: 'a
impliesR ⊢ (Θ T): 'a
, which implies
- First, we show that if
R ⊢ Pi: 'a
, then every "subcomponent"P'
ofPi
outlives'a
. The idea here is that each variable変数、ストレージXi
from the impl will match一致する、マッチさせるagainst and extract抽出するsome subcomponentP'
ofPi
, and we wish to show that the subcomponentP'
extracted抽出するbyXi
outlives'a
. - Then we will show that the type
θ T
outlives'a
if, for each of the in-scope parameters仮引数Xi
,Θ Xi: 'a
.
DefinitionConstrained(T)
defines the setX::Foo
does not constrain what type X
can takeX
as an input to compute a result:
Constrained(scalar) = {}
Constrained(X) = {X}
Constrained(&'x T) = {'x} | Constrained(T)
Constrained(O0..On+'x) = Union(Constrained(Oi)) | {'x}
Constrained([T]) = Constrained(T),
Constrained(for<..> fn(T1..Tn) -> T0) = Union(Constrained(Ti))
Constrained(<P0 as Trait<P1..Pn>>::Id) = {} // empty set
DefinitionConstrained('a) = {'a}
. In other words, a lifetime reference
Lemma 1: GivenR ⊢ P: 'a
, P = [X => P'] Q
, and X ∈ Constrained(Q)
, then R ⊢ P': 'a
. ProceedP
:
- If
P
is a scalar or parameter,仮引数there are no subcomponents, soP'=P
. - For nominal types, references,参照objects, and function types, either
P'=P
orP'
is some subcomponent ofP
. The appropriate "outlives" rules all require that all subcomponents outlive'a
, and hence the conclusion follows下記の、次に続く、追従するby induction. - If
P'
is a projection, that implies暗黙のthatP'=P
.- Otherwise,さもなければ
Q
must be a projection, and in that case,Constrained(Q)
would be the empty空のset.セットする、集合
- Otherwise,
Lemma 2: GivenFV(T) ∈ X
, ∀i. Ri: 'a
, then [X => R] T: 'a
. In other words, if all the type/lifetime parameters'a
, then the type outlives 'a
. Follows
Edit History
RFC1592 - amend to require that tuple