Open scalexm opened 5 years ago
While reading this, I found myself wondering about RFC 2027, which aims to tweak the rules around dyn Trait
being "well-formed" a bit. Might be relevant. Have to go revisit the RFC first to decide =)
@nikomatsakis I guess for that RFC, we could just move the ObjectSafe
condition from the WF rules onto the generated impls (in the where clauses of the generated impls, presumably)
Edit: wouldn't work because we require WellFormed(dyn Foo) :- WellFormed(dyn Foo: Foo)
and WellFormed(dyn Foo: Foo)
would not hold if the trait is not object safe
@scalexm and I discussed RFC 2027 on Zulip here. We came to the conclusion (roughly around here) that it should be compatible, but we have to be a bit careful with the setup.
We need to design rules for the well-formedness of trait objects that address the soundness issue https://github.com/rust-lang/rust/issues/44454 and that are future-proof under implied bounds.
In what follows, we'll write
dyn Trait
to actually denotedyn Trait + 'a
for a placeholder lifetime'a
, the rules will work regardless of'a
.Trait objects in rustc
Say we have the following trait:
We will assume that this trait has no methods -- methods only change a bit the definition of "object-safety". Then we say that the trait is object-safe if:
Self
does not appear as a non-self type parameter in the where clausesWC1
, ...,WCn
or in the where clauses of any super-trait ofFoo<T1...Tk>
Sized
is not a transitive super-trait ofFoo<T1...Tk>
Then
dyn Foo<...>
is considered well-formed ifFoo
is object-safe. Example of object-safe traits:Example of non-object safe traits:
Then, whenever a trait
Foo<T1...Tk>
is object-safe, for each super-traitSuperTrait<...>
ofFoo<T1...Tk>
(includingFoo<T1...Tk>
itself), the compiler unconditionally generates the following impl:Why the previous approach is unsound
The unsoundness comes from the generated impls above. For example,
dyn Foo<&'a i32>
in this example is a well-formed trait object:and we have that
dyn Foo<&'a i32>: Foo<&'a i32>
even ifdyn Foo<&'a i32>
does not outlive'static
.Another example which may seem weird:
Note that with this
X: Copy
example, you cannot do anything useful because Rust does not support fully general implied bounds for now, however we must keep this example in our head if we want do design something which works with implieds bounds.However, because Rust does support implied bounds for outlive requirements, here is what you can do with the first example:
(playground)
This works because in
only_foo
, we bound overT: Foo<X>
, so we get an implied boundT: 'static
as per the definition oftrait Foo<X>
. And because outlive requirements are syntactical, ifT: 'static
, then rustc deduces that<T as Projector>::Item
must be'static
.Starting from there, we may confuse the compiler enough to create memory unsoundness (see some examples in https://github.com/rust-lang/rust/issues/44454).
Proposed rules for a prototype in chalk
The problem with the previous approach comes from the fact that the generated impls are unconditional, and that we can name types that should obviously not be well-formed. Basically, a
dyn Foo<X>
is just an existential type?Self
such that?Self: Foo<X>
holds. So we may legitimately assume that all things talking directly about?Self
are true (e.g. that?Self
implements all super-traits oftrait Foo<X>
). However, because we have lost the information of where exactly the type parameterX
appears in?Self
(note that it may not appear at all), we cannot assume anything neither aboutX
nor about any outlive requirements on?Self
.Here are a set of rules which I think solve the problem. First, we keep the definition of object-safety, and we'll say that the special domain goal
ObjectSafe(Trait)
holds iftrait Trait<T1...Tk>
is object-safe. We may either encode the rules forObjectSafe(Trait)
in the logic or pre-compute it for each trait, I believe either method works.Now, suppose we have the following trait:
and let
SuperTrait<...>
be any transitive super-trait ofFoo<T1...Tk>
. IfSuperTrait<...>
comes from the following definition:then we generate the following impl:
where
SuperWC1[...]
just means that we have substitutedSuperWC1
from the trait definition with the right type parameters. Note again that in the set of the super-traits ofFoo<T1...Tk>
, we haveFoo<T1...Tk>
itself.Finally, we have the following rule for the well-formedness of
dyn Foo<X1...Xk>
:I believe that this solution can be implemented in rustc today.
Note that, in the rules described earlier, we added where clauses from the super traits on every generated impl only so that the impl is well-formed even without implied bounds, I am not sure they are strictly needed.
Examples
In the above rules, the fact that we generate an impl of
SuperTrait<...>
fordyn Foo<T1...Tk>
encodes the previous claim:while the fact that we are retaining where clauses from
SuperTrait<...>
on the generated impl encodes the other claim:Let's walk through some examples to make that clearer.
In the above, the well-formedness of
dyn Foo<X>
only holds ifX: 'static
or more generally if you can prove thatdyn Foo<X>: 'static
.In the above,
dyn Foo<i32>
is WF but notdyn Foo<String>
.Implied bounds
Because of the well-formedness rule, we naturally have the following implied bound:
This permits writing things like:
With this reverse rule and implied bounds, the where clauses from super-traits on the generated impls are definitely not needed.
A curious example
Today in Rust, you can have the following trait:
The trait
Foo
is object-safe anddyn Foo
is well-formed. However, the following code is not accepted:However, we may add the following (manually written) impl:
In which case we can now call
foo::<dyn Foo>()
.With my proposal, what would happen is that in the absence of
impl Bar for Box<dyn Foo>
, thendyn Foo
would not be considered well-formed, becauseBox<Self>: Bar
is not really a super-trait. Withimpl Bar for Box<dyn Foo>
, then everything works again.Question is: should we try to extend the generated impls to all bounds, so that we don't need to write the
impl Bar for Box<dyn Foo>
impl? Note that generating the impls for all the transitive super-traits must be done outside of the logic, but is easy. However, generating impls for all bounds would require some kind of fixed-point algorithm. Moreover, this may be even harder for traits with bounds on associated types, like in:Multi-trait objects
Note that this proposal naturally extends to multi-trait objects. For example, you'd have:
while the generated impls would come from super-traits of both
Foo
andBar
.