I have a draft document[1] laying out a possible vision for (allegedly) sound specialization. The connecting thread between all such systems is that the bounds used for specialization MUST NOT be potentially dependent on any lifetime bounds, as that would be unsound[2]. Thus, because trait impl
s can introduce lifetime bounds, specialization cannot be bound on trait bounds.
The solution to this conundrum is what @nikomatsakis called “always applicable impls” — impls which introduce no bounds which are not entirely structural (i.e. are present on the type definition, and would be implied by the implied bounds RFC). Aside: note that impl Drop
has always been required to be always applicable, so this concept already exists in the language. As part of feature(min_specialization)
, Rustc defines an attribute #[rustc_specialization_trait]
for marking “specialization safe” traits where all impls are required to be “always applicable” (e.g. Sized
) and then allows specialization to be bound on these traits. (My notes call this virtual trait
, FWIW, as the keyword is available and vaguely appropriate.) Rustc also defines a separate #[rustc_unsafe_specialization_marker]
which doesn't have the “always applicable” impl requirement as an unsafe escape hatch (e.g. for Copy
).
What if: in a future edition, we mark all the marker traits as “specialization safe” and require all new impls of those traits to be always applicable? For compatibility with old editions, under the hood the compiler would remember if an impl is required to be always applicable and only use it for specialization bounds if so[3]. Given we'd want to lint impls in old editions which aren't new edition compatible, it'd be possible to have any always applicable from old editions (e.g. via #[derive(Copy)]
) also participate in specialization, minimizing the surface area of impls that exist but don't get specialized on.
IIUC, the ability to have explicit Copy
implementations that aren't specialization safe is mostly regarded as an unfortunate reality, rather than intentional or useful[4]. Removing that possibility to close a reasonably well-known soundness hole[5] is potentially justifiable.[6]
Alternatively, make all editions behave the same and merely warn for non-specialization-safe impls instead of erroring. Or perhaps make participating in specialization explicitly opt-in; perhaps virtual impl
? (Using virtual
in the sense of “participates in specialization” which, while not the OOP virtual
, I think is compatible meaning to it.)
Far from ready to be seen, unfortunately; what notes I do already have are barely comprehensible to me, and even then only because they prompt me to remember the various ideas I've had, rather than actually stating useful domain information and/or context. ↩︎
This is because lifetimes are not (and cannot be) monomorohically compiled. Because of this, a single lifetime-polymorphic function cannot possibly select which of a called lifetime-dependent specialization set to invoke. I suppose we could “just” declare that invoking a more general implementation in a specialization set is “sound,” and we could even potentially exactly specify how lifetime-dependent specialization selection works, but nobody is particularly keen on calling such pseudo-nondeterminism “sound.” And it'd require the compiler being particularly careful to select the more general implementation instead of the more strongly bound one, to avoid actual unsound lifetime extension (IIRC the current trait selection can do this.) ↩︎
Potentially, and as if, via splitting the traits into the current and a
TrustedMarker
version, like is done in std specialization already in practice. (An internal unstableStructuralClone
and/orTrustedCopy
could work to remove#[rustc_unsafe_specialization_marker]
fromCopy
today, if that's considered desirable.) ↩︎Even the case of a typestate marker recording that type-erased data is[n't]
Copy
can straightforwardly be specialization safe by making their custom marker trait also specialization safe. ↩︎Although in fairness, one that's quite difficult to actually exploit and turn into unsoundness in practice, since it requires a conditionally-
Copy
type where copying a non-Copy
instance instead of cloning it is in some way unsound and not only logically incorrect. ↩︎It could, however, likely mean that I'd need to replace my habit of writing things like
unsafe impl<T> Sync for MyType<T> where Arc<T>: Sync {}
with writing the elaborated, more clearly specialization safe form likeunsafe impl<T> Sync for MyType<T> where T: Send + Sync {}
instead. (The former only adds specialization safe bounds, so should be specialization safe, but the mention of an unrelated type makes that non-obvious.) ↩︎