But when lifetime bounds are "in scope" isn't necessarily a question with an obvious answer, so even if it's deterministic for a given version of the compiler, it could appear random to even a fairly knowledgeable developer.
I suspect that by "in scope," you mean determinable by only the immediate generic binder of the function doing the specialization, as I think that's the only precisely definable set to perform specialization for. But if that's the case, isn't it possible to just use the more specific case, since it's always applicable?
If you want specializes_on(static_cow) to actually specialize, then we're applying specialization during generic instantiation. Rustc currently does no real generic instantiation (only a small subset of checks for wf guarantees for some type instantiations), and at the point monomorphization happens and the generic selection actually occurs after all lifetime info has been erased. That's the problem with any lifetime-dependent specialization.
We could say that instantiation of specializations occurs as a part of MIR inlining. (That's actually a rather promising angle, IMO.) But what gets inlined, especially at the MIR level, is effectively opaque to the developer (thus seemingly random).
Furthermore, it's not as simple as saying that the specialized impl needs to be "always applicable;" instead, it needs to be "always applicable" from a baseline bound of whatever the specialization context is. As a trivial example, given T: '_, it would be unsound to specialize on T = &'static str, but if we're given T: 'static, then T = &'static str is now a sound specialization.
Specialization that only works when satisfying impl lifetime bounds aren't explicitly bound is still a 90% solution, I agree, but I also think exploring what the 99% solution would look like is a worthwhile venture. Perhaps (with an exception of some core traits) it's a reasonable expectation for there to typically be a separate trait opt-in to specialization, that enforces the "always applicable" requirements.
Since these specific traits are typically derived, a specializable CopySpec/DebugSpec would day 1 cover most (but not all) types.
But, note, std is experimenting with TrivialClone now, with the soundness constraint that Clone is a simple copy, even if the type is not Copy. So that types like ops::Range<i32> can benefit from the library level clone elision specialization.
making them specializable over an edition
Both Copy and Debug are good candidates for wanting to be specializable. This could happen mostly transparently with something along the lines of:
trait Debug2015 {
// elided…
}
spec trait Debug20XX: spec Debug2015 {
// read: specialization trait `Debug20XX` which provides
// specialization access to the `Debug2015` bound
// this item purposefully left blank
}
impl Debug20XX for dyn Debug2015 + '_ {}
trait Debug = builtin#magic! {
if in specialization bound => Debug20XX,
if min_edition = "202X" && in `impl for` =>
Debug20XX + Debug2015, // impl both in one block
otherwise => Debug2015,
}
derive(Debug) on all editions derives both traits, with generics bound on the respective trait.
For Copy, though, extending TrivialClone library optimizations to non-Copy types seems "worth it" enough that those should be separate concerns.
…I forgot to factor Christmas into my estimate for blog post publishing. But it's almost ready, hopefully.