-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement RFC 1210: impl specialization #30652
Conversation
🎊 🎈 🎊 Regardless which way the RFC goes, this is tremendous work @aturon, big props! |
Obviously, needs a rebase :) Also, I forgot to mention, but this passes the full existing test suite as well as a pile of new tests, so it should be in a pretty good state. |
Regarding the gaps:
This should be pretty easy to add in a follow-up, given the infrastructure laid by this PR.
I'm really not sure about this one; right now, inherent impls work pretty independently from our trait infrastructure, so this is potentially a major refactor. It's also one of the more subtle bits of the design. I think we should probably sit on this for a while.
Should be easy to add; can probably sneak it into this PR.
I'm not actually sure why this isn't working yet; somehow the selection infrastructure is failing to elaborate supertrait relationships. Probably an easy fix? |
342a9d9
to
99e78f7
Compare
Here's a concise example of a problem I've found. This code compiles fine: use std::marker::PhantomData;
trait Id_ {
type Out;
}
type Id<T> = <T as Id_>::Out;
impl<T> Id_ for T {
type Out = T;
}
fn coerce<A>(_: PhantomData<A>, x: A) -> A { x }
#[test]
fn test() {
type Bool = Id<bool>;
let x = coerce(PhantomData::<Bool>, true);
} But this, with the only difference being the "default" keyword, does not: use std::marker::PhantomData;
trait Id_ {
type Out;
}
type Id<T> = <T as Id_>::Out;
impl<T> Id_ for T {
default type Out = T;
}
fn coerce<A>(_: PhantomData<A>, x: A) -> A { x }
#[test]
fn test() {
type Bool = Id<bool>;
let x = coerce(PhantomData::<Bool>, true);
} The error reported is "associated type expected, found bool" in the second argument to the call to |
@dylanede Yep, looks like a bug. I'm a bit slammed with other things but I'll try to add this as an additional test for this PR :) |
Another thing I've noticed. In some cases if an associated type specialization of an EDIT: actually, this appears to be every time. |
For example, this code gets the ICE: trait Foo {
type Out;
}
impl Foo for bool {
type Out = ();
}
impl<T> Foo for T {
default type Out = bool;
} |
Another, different ICE: trait Id_ {
type Out;
}
type Id<T> = <T as Id_>::Out;
impl<T> Id_ for T {
default type Out = T;
}
fn main() {
let x: Id<bool> = panic!();
} Error and backtrace:
If the |
Similarly, if I try |
⛵ |
@dylanede BTW, are you following the thread on the RFC? We are moving toward removing specialization of associated types. |
@dylanede (In particular, I'm now in the process of rebasing and addressing the initial review, and plan to remove the code for associated type specialization.) |
@aturon Ah, I hadn't been. Bear in mind that if you remove specialisation of associated types, the metaprogramming capabilities are pretty much completely removed. As it is I don't think Rust's type system is Turing Complete at the moment (ever since the changes that added the rule that impl type parameters must appear in the parameters for the trait being impl'ed or the type being impl'ed for) - I have repeatedly tried and failed to implement the untyped lambda calculus in it. With proper associated type specialisation, the implementation of ULC is almost trivial. I currently have prototype libraries waiting on better metaprogramming support, as the type system as it is doesn't seem capable enough - an Earley-based CFG parsing library and an expression-template style linear algebra library with similar aims to Eigen for C++. |
@dylanede So, part of the debate on the RFC is whether, given the constraints we have to put on projections, specializable associated types are all that useful. If you could leave a comment on thread giving some concrete details of your use case, that'd be very helpful! |
As I understand it, @dylanede's use case is type-level metaprogramming. This allows for design patterns currently not possible in Rust, such as expression templates. Expression templates can achieve massive performance improvements (they are analogous to stream fusion in Haskell). |
Keep in mind that stream fusion is unnecessary in Rust, as lazy lists (which can be inadvertently fully evaluated) were replaced by "strictly lazy" streams ( So I'd take "massive" with a grain of salt, but that's just me. |
1e9ab7d
to
4b09f0c
Compare
I've now rebased and pushed a commit that addresses the more basic nits you brought up. There are several more things that need to happen before landing:
I'm hoping to have all these addressed before the RFC is accepted, but @nikomatsakis I need your help on several of these points. |
Oh, also, my branch is currently failing some dependency graph tests, and I don't fully understand why. |
4b09f0c
to
efd700d
Compare
☔ The latest upstream changes (presumably #31487) made this pull request unmergeable. Please resolve the merge conflicts. |
…reduce chance of bugs
@bors: r=nikomatsakis |
📌 Commit 6562eeb has been approved by |
⌛ Testing commit 6562eeb with merge 9ca7561... |
Implement RFC 1210: impl specialization This PR implements [impl specialization](rust-lang/rfcs#1210), carefully following the proposal laid out in the RFC. The implementation covers the bulk of the RFC. The remaining gaps I know of are: - no checking for lifetime-dependent specialization (a soundness hole); - no `default impl` yet; - no support for `default` with associated consts; I plan to cover these gaps in follow-up PRs, as per @nikomatsakis's preference. The basic strategy is to build up a *specialization graph* during coherence checking. Insertion into the graph locates the right place to put an impl in the specialization hierarchy; if there is no right place (due to partial overlap but no containment), you get an overlap error. Specialization is consulted when selecting an impl (of course), and the graph is consulted when propagating defaults down the specialization hierarchy. You might expect that the specialization graph would be used during selection -- i.e., when actually performing specialization. This is not done for two reasons: - It's merely an optimization: given a set of candidates that apply, we can determine the most specialized one by comparing them directly for specialization, rather than consulting the graph. Given that we also cache the results of selection, the benefit of this optimization is questionable. - To build the specialization graph in the first place, we need to use selection (because we need to determine whether one impl specializes another). Dealing with this reentrancy would require some additional mode switch for selection. Given that there seems to be no strong reason to use the graph anyway, we stick with a simpler approach in selection, and use the graph only for propagating default implementations. Trait impl selection can succeed even when multiple impls can apply, as long as they are part of the same specialization family. In that case, it returns a *single* impl on success -- this is the most specialized impl *known* to apply. However, if there are any inference variables in play, the returned impl may not be the actual impl we will use at trans time. Thus, we take special care to avoid projecting associated types unless either (1) the associated type does not use `default` and thus cannot be overridden or (2) all input types are known concretely. r? @nikomatsakis
🎋 |
This PR implements impl specialization,
carefully following the proposal laid out in the RFC.
The implementation covers the bulk of the RFC. The remaining gaps I know of are:
default impl
yet;default
with associated consts;I plan to cover these gaps in follow-up PRs, as per @nikomatsakis's preference.
The basic strategy is to build up a specialization graph during
coherence checking. Insertion into the graph locates the right place
to put an impl in the specialization hierarchy; if there is no right
place (due to partial overlap but no containment), you get an overlap
error. Specialization is consulted when selecting an impl (of course),
and the graph is consulted when propagating defaults down the
specialization hierarchy.
You might expect that the specialization graph would be used during
selection -- i.e., when actually performing specialization. This is
not done for two reasons:
we can determine the most specialized one by comparing them directly
for specialization, rather than consulting the graph. Given that we
also cache the results of selection, the benefit of this
optimization is questionable.
selection (because we need to determine whether one impl specializes
another). Dealing with this reentrancy would require some additional
mode switch for selection. Given that there seems to be no strong
reason to use the graph anyway, we stick with a simpler approach in
selection, and use the graph only for propagating default
implementations.
Trait impl selection can succeed even when multiple impls can apply,
as long as they are part of the same specialization family. In that
case, it returns a single impl on success -- this is the most
specialized impl known to apply. However, if there are any inference
variables in play, the returned impl may not be the actual impl we
will use at trans time. Thus, we take special care to avoid projecting
associated types unless either (1) the associated type does not use
default
and thus cannot be overridden or (2) all input types areknown concretely.
r? @nikomatsakis