Yet another stupid thought about specialization

Hehey yeah it's the most beloved topic: specialization.

Up front, I want to say that I don't have deep knowledge about how the rust compiler really works, but it was too tempting to write this blog post, so yeah.. you are free to call me stupid after this.

Why I even need specialization

Like some other people... I figured that I need it. But I don't really need it for any cool optimization or "special implementation", instead I want to do some sort of type level Set, and that has lead to some interesting thoughts.

So the idea is simple. You have () and (Down, T) as the two implementors for Set, where Down: Set. This will enable me to have a value like (((), "hello"), 42) and then get "the most outside &str" or "the most outside i32". This, however, requires me to have two conflicting implementations, because I can't say that SetT = SearchT weighs more than Down: Has<T>.. unless, of course, with specialization involved. But that is unsound, so I sought help, of which you can find the result here. Long story short, I've reached the limits of what the propsed version there enables and now need real specialization, but also got an interesting idea from that solution.

Now what was the problem with specialization again?

Right... the problem with specialization and lifetimes. The most basic example from aturon's blog is the following:

trait Bad1 {
    fn bad1(&self);
}

impl<T> Bad1 for T {
    default fn bad1(&self) {
        println!("generic");
    }
}

// Specialization cannot work: trans doesn't know if T: 'static
impl<T: 'static> Bad1 for T {
    fn bad1(&self) {
        println!("specialized");
    }
}

fn main() {
    "test".bad1()
}

trans can't tell if the more specialized implementation is satisfied.

The Idea

While others have suggested to make trans smarter, I have come up with a different idea. At least to me, it seems like while trans doesn't know about lifetimes, typeck does, and thus also the part that is responsible for picking traits.

The idea I have in mind is simple:

Because monomorphization is already capable of doing different stuff on trait implementations, e.g.

trait A {
    fn get_name(&self) -> &'static str;
}

impl A for i32 { .. }
impl A for i16 { .. }

fn function(a: &impl A) {
    let _ = a.get_name(); // this could call two different methods
}

what is stopping us from extending that then?

What I mean by that is having two mutually exclusive sets represented by a specialized trait, where we already categorize the type into "this specialization applies" and "doesn't apply" at the time of typeck. For my experiments, I have named this trait Is<T>.

Is<T> now has an associated type, called Really, which is only set to either Yes or No.

Those are for now implemented with the current #![feature(specialization)], but I don't know how safe that makes it as I'm unsure where the picking happens in that case.

Coming back to the Set and Has<T> example, it looks like this and can successfully differentiate between &'static str and &'a str.

Cool - now have I just solved the specialization problem? I don't know, but my brain dug out some other issue I should try to come over: Specialization and lifetime dispatch #40582

Show stopper: I didn't achieve it.

.....buuuuuuuuuuuut I still think it's doable!

Now what was the blocker?

In my experiment the compiler for some reason inferred that when I enforce T: 'static, the lifetime 'a in &'a T must also be 'static - at least that's what I think is happening. A shorter reprod of this mistake can be observed here.

Prologue

Now, after this, I wonder:

  • Does the implied 'static bounds have any sense I've overseen or can it be removed?
  • With the implied : 'static removed, does this idea really work?
  • Does at least the general idea work after some more changes, e.g. putting some of the choosing stuff from trans more into typeck?

More shenanigans I've found while experimenting:

  • playground The reason for why T should be : 'static is on the wrong span, one type argument later.
  • playground Changing the associated type to an associated bool const together with #![feature(associated_const_equality)] and #![feature(generic_const_exprs)] makes the compiler error

I have no clue whether either of these issues are known, or how I could possibly find out if they are. The only thing that came to mind was a simple github issue search, which however didn't yield any results. So I'm instead just gonna put them here and see what happens :sweat_smile:.

Okay.. I tried to just hack out the 'static error, which still ended in it believing it always gets a 'static.. but hacking on rustc was definitely easier than I thought.

I have to look a bit closer in what happens but I might have another idea I want to try, which will take some more work in rustc but I feel ready for that.

Other than impl work, the main reason it's not that simple to “just” remember which overlapping impl to select is that while generic types get monomorphized, generic lifetimes don't.

If a function is only generic over a lifetime, only one[^1] version of the function gets generated. And this is observable in the surface language because of for<‘a> fn(&'a str) HRB function pointers.

[^1]: Ignoring inlining and duplication between codegen units.

I'm of the firm belief that only allowing specialization for bounds that are known at the point of authorship impossible to specialize on lifetimes is the way to go, that this restriction is not too much of a restriction, and that I've determined exactly[^2] what that restriction looks like[^4] for surface level code. I'm working[^3] on a trio of blog posts that lay out the restrictions. (Part one: ignore the problem, establish my idea of the what and how of specialization with only specialization on const generics. Part two: introduce the safety test needed for generic specialization, and use it to allow impl specialization on types, including generic ones, but excluding trait bounds. Part three: specialization safety for trait bounds and everything that requires.)

[^2]: Well, almost. I'm still trying to figure out how to express the difference between #[specializable] trait SpecTrait: Trait where you want to specialize a base case of T: Trait or where you want to specialize a base case without a Trait bound where Trait itself is not specialization safe.

[^3]: At this point it's mostly just blocked on finding concurrent time and motivation to finish the writing/editing. I plan to release all three parts at the same time; it's fairly necessary, and might turn into one big post with sections instead, depending on how editing goes.

[^4]: The TL;DR is extending the prior discussed concept of “always applicable impls” to explicitly mark traits that must only have always applicable impls, along with marking always applicable generic type bounds to handle the new semver requirement that generic bounds must not be relaxed in a way that can change downstream specialization safety.

2 Likes

Thanks, I’ll look forward to the blog posts!

About the codegen, I already figured that, i was just unsure when the splitting really happens and thought I could trick it when I give the type more info about the specialization which is already inferred in typeck, but that still only generates one unit.

My next step will be trying to hack on the rust compiler, as that’s far easier than I thought, and try to get a PoC to not make trans lifetime-aware, but still specialization-aware by giving information about which impl triggers from tyepck down, if that sounds reasonable.

For „raw“ function pointer functions, that sounds like a stupid problem :confused: I only ever thought about generic and lifetime aware as that’s what I need, but I wonder if one could just say „one must always be aware that when only lifetimes matter, the more general impl could always be called, even if there’s a more specialized“ - or are you planning on forbidding raw function pointers when the function uses traits marked with specializable (however that will work without getting body context)?

Also - is there some other place where specialization discussion happens? I just thought it was dead but it seems like there are at least some people still working on it :smiley: