Inline specialization

Warning: This is a half-baked idea.

There are many soundness issues with specialization in its current form. I would like to propose an alternative. While it may not be as powerful, it should be sound due to its simplicity.

where clause statement

Typically, specialization is used to implement special behavior for types implementing a specific trait. Instead of replacing the impl with a new one (requiring trait selection to be vigilant wrt overlapping impls), I propose a statement with similar semantics as C++ if constexpr, but exclusively for trait bounds:

pub trait CloneInto: Clone {
  // An implementation on Vec can reuse the existing allocation.
  fn clone_into(&self, dst: &mut Self);
}

pub fn clone_into<T: Clone>(value: &T, dst: &mut T) {
  where T: CloneInto {
    // Within this block, T also implements CloneInto.
    value.clone_into(dst)
  } else {
    // Otherwise, fall back to Clone.
    *dst = value.clone();
  }
}

To summarize:

  • At compile time, if the specified bounds are true the given block is used. Others are discarded.
  • The types implement traits from the where bounds within the block.
  • All bounds are supported here.
  • If possible, simple type equality bounds could be supported.
  • Like if statements, where clause statements also have else and else where statements.

One downside (upside) is that this does not modify trait selection. While this is strictly less powerful than trait-level specialization, it meets most of the demand and doesn't touch the trait solver (which seems to be a common way to cause unsoundness).

More concretely, this still does not allow blanket trait impls to be overridden (which is why clone_into is a free fn). Specialization on blanket trait impls can be emulated by using two traits: one for the blanket impl, and one for the specialization.

While try_as_dyn also exists, I think this approach supercedes it. The traits do not need to be dyn compatible, so traits like Default or bytemuck marker traits like Zeroable can be specialized upon.

Is the query T: CloneInto even decidable, to allow the else branch to assume that T: !CloneInto? It might not be (it is semidecidable though), then your code can non-deterministically go to the else branch...

1 Like

Not to mention that any generic callers of clone_into would need a T: ?CloneInto (or something) to pass along the "does it impl CloneInto" bit as they'd otherwise all fall into the else block. If the T is inspected regardless of the caller's bounds, you have:

fn is_static<T>() -> bool {
    where T: 'static {
        true
    } else {
        false
    }
}

which I'm sure someone could use to be unsound where they only know T: 'a.

2 Likes

This has already been discussed multiple times, e.g. in this recent thread. Given that:

  • this approach by itself does not fix specialization's unsoundness. That comes from specializing code on lifetimes, and this approach still allows that (consider if I e.g. do impl CloneInto for &'static MyType, your code would have to detect that lifetime at monomorphization time). The approach that try_as_dyn takes is to sometimes return None (in your case take the else branch) even if the type does implement the given trait.

  • try_as_dyn does not need to interact with the language syntax, nor does it interact with the param env in functions. This makes it simplier to implement and experiment with. Nothing prevents the final feature to look like what you described, but for experimenting a library-only solution is much more practical.

2 Likes

I'm sorry, but when discussing a solution for a problem that is so widely explored and discussed (and is actively worked on), I believe it makes sense to understand what the blockers are. Which if you would do, you would realize that the problem is not the syntax, your approach is not materially better than try_as_dyn() (and was already proposed FWIW), and this post does nothing to solve the hard problems.