rust-lang/rust

Tracking issue for negative impls

nikomatsakis opened this issue Β· 46 comments

Generalized negative impls were introduced in #68004. They were split out from "opt-in builtin traits" (#13231).

Work in progress

This issue was added in advance of #68004 landed so that I could reference it from within the code. It will be closed if we opt not to go this direction. A writeup of the general feature is available in this hackmd, but it will need to be turned into a proper RFC before this can truly advance.

Current plans

  • Forbid conditional negative impls or negative impls for traits with more than one type parameter (#79098)
  • Forbid !Drop (negative impls of Drop)

Unresolved questions to be addressed through design process

  • What should the WF requirements be? (Context)
  • Should we permit combining default + negative impls like default impl !Trait for Type { }? (Context)

Odd possibly off topic question about this. The following compiles:

#![feature(negative_impls)]

pub struct Test{

}

impl !Drop for Test {}

fn foo(){
    drop(Test{})
}

Should it?

Odd possibly off topic question about this. The following compiles:

#![feature(negative_impls)]

pub struct Test{

}

impl !Drop for Test {}

fn foo(){
    drop(Test{})
}

Should it?

I would say no, because if you wanted to shift the error to runtime you could implement Drop as:

pub struct Test{

}

impl Drop for Test {
    fn drop() {
        panic!("Do not drop Test.")
    }
}

I generally like to catch anything at compile time that can be caught at compile time.

According to the documentation

This function is not magic; it is literally defined as

pub fn drop<T>(_x: T) { }

As a trait bound, T: Drop means that the type has defined a custom Drop::drop method, nothing more (see the warn-by-default drop_bounds lint). It does not mean that T has non-trivial drop glue (e.g. String: Drop does not hold). Conversely, T: !Drop only says that writing impl Drop for T { … } in the future is a breaking change, it does not imply anything about T's drop glue and definitely does not mean that values of type T cannot be dropped (plenty of people have tried to design such a feature for Rust with no success so far).

(If you did not already know, drop glue is the term for all the code that std::mem::drop(…) expands to, recursively gathering all Drop::drop methods of the type, its fields, the fields of those fields, and so on.)

Yes, the Drop trait is very counter-intuitive. I suggest extending drop_bounds to cover !Drop as well. Maybe even mentioning !Drop at all should be a hard error for now (neither impls nor bounds make sense IMO). Can @nikomatsakis or anyone else add this concern to the unresolved questions section in the description?

Not sure if this is the right place to report a bug with the current nightly implementation, but a negative implementation and its converse seem to "cancel each other out".

For example:

trait A {}
trait B {}

// logically equivalent negative impls
impl<T: A> !B for T {}
impl<T: B> !A for T {}

// this should not be possible, but compiles:
impl A for () {}
impl B for () {}

The above compiles without errors on nightly, though it shouldn't. Removing one of the negative impls fixes the issue and results in an error as expected.

Allowing negative trait bounds would make my code much better by allowing to express di- and poly-chotomy.

In Rust 1.57.0 the following doesn't compile:

#![feature(negative_impls)]

trait IntSubset {}

impl <T> IntSubset for T where T: FixedSizeIntSubset + !ArbitrarySizeIntSubset {}
impl <T> IntSubset for T where T: !FixedSizeIntSubset + ArbitrarySizeIntSubset {}

trait FixedSizeIntSubset {}

impl<T: FixedSizeIntSubset> !ArbitrarySizeIntSubset for T {}

trait ArbitrarySizeIntSubset {}

impl<T: ArbitrarySizeIntSubset> !FixedSizeIntSubset for T {}

Coming here from a compiler error, how do I "use marker types" to indicate a struct is not Send on stable Rust? I don't see any "PhantomUnsend" or similar anywhere in std.

error[E0658]: negative trait bounds are not yet fully implemented; use marker types for now

@mwerezak I think I've seen PhantomData<*mut ()>. If I understand correctly, *mut () is not Send, so neither is the enclosing PhantomData nor your struct. (Did not test)

PhantomUnsend might be a good alias/newtype for better readability – I had no idea what PhantomData<*mut ()> meant at first :D

@rMazeiks Thanks, I wouldn't have known really what type would be the appropriate choice to use with PhantomData for this.

A PhantomUnsend sounds like a good idea.

I think we should just rule out !Drop -- drop is a very special trait.

Can we add "negative super traits" to the unwritten RFC? That is,

pub trait Foo {}

pub trait Bar: !Foo {}

// We now know that the two traits are exclusive, thus can write:
trait X {}
impl<T: Foo> X for T {}
impl<T: Bar> X for T {}

Quote from the Hackmd:

This implies you cannot add a negative impl for types defined in upstream crates and so forth.

Negative super traits should get around this: the above snippet should work even if Foo is imported from another crate.

The potential issue here is that adding implementations to any trait exported by a library is technically a breaking change (though I believe it is already in some circumstances due to method resolution, and also is with any other aspect of negative trait bounds).

I would prefer to leave that for future work, @dhardy -- I'd rather not open the door on negative where clauses just now, but also I think that it'd be interesting to discuss the best way to model mutually exclusive traits (e.g., maybe we want something that looks more like enums).

Should manually implementing !Copy be allowed (#70849 #101836)? I assume it should.

@fmease when is Copy ever implemented automatically?

@Alxandr Never, I know. This is just a corner case lacking practical relevance I think. I am just asking here to be able to decide whether the issue I linked is a diagnostics problem only or if the compiler is actually too strict / lax. There should be no harm in implementing !Copy as a signal to library users.

Edit: There might even be some benefit in doing that with negative coherence enabled.

Bajix commented

I think we should just rule out !Drop -- drop is a very special trait.

I found a use case for this while working on async-local; for types that don't impl Drop, the thread_local macro won't register destructor functions, and the lifetime of these types can be extended by using Condvar making it possible to hold references to thread locals owned by runtime worker threads in an async context or on any runtime managed thread so long as worker threads rendezvous while destroying thread local data. For types that do impl Drop, they will immediately deallocate regardless of whether the owning thread blocks and so synchronized shutdowns cannot mitigate the possibility of pointers being invalidated, making the safety of this dependent on types not implementing Drop.

Conditional negative impls seem to be broken?

#![feature(auto_traits, negative_impls)]

unsafe auto trait Unmanaged {}

unsafe trait Trace {}

struct GcPtr(*const ());

unsafe impl Trace for GcPtr {}

// It seems like rustc ignores the `T: Trace` bound.
impl<T: Trace> !Unmanaged for T {}

fn check<T: Unmanaged>(_: T) {}

fn main() {
    let a = (0, 0);
    // error: the trait bound `({integer}, {integer}): Unmanaged` is not satisfied
    check(a);
}
mejrs commented

Conditional negative impls seem to be broken?

As far as I can tell this has never had defined semantics. See #79098 also.

This came up in the libs-api meeting today while we were discussing rust-lang/libs-team#175 which proposes adding PhantomUnSend and PhantomUnSync types to the standard library. We feel that it would be better to solve the ergonomics issue (using PhantomData to opt out of Send/Sync is... ugly) with a proper language feature.

How does the lang team feel about a partial stabilization of negative impls which only covers the specific use case of opting-out of OIBITs like Send/Sync/Unpin`?

For those who's wondering what OIBITs are,

The acronym β€œOIBIT”, while quite fun to say, is quite the anachronism. It stand for β€œopt-in builtin trait”.

Source: https://internals.rust-lang.org/t/pre-rfc-renaming-oibits-and-changing-their-declaration-syntax/3086

Also see https://github.com/rust-lang/rfcs/blob/master/text/0019-opt-in-builtin-traits.md.

We discussed this in today's @rust-lang/lang meeting. We had a consensus in favor of stabilizing enough of negative impls here.

The subset we'd be in favor of stabilizing would be to require that the negative impl be "always applicable". Effectively, the impls can't have any bounds other than those required for an instance of the type. So, if struct Foo<T> requires T: Debug then you can impl<T> !Send for Foo<T> where T: Debug, but if struct Foo<T> doesn't require T: Debug then you can only impl<T> !Send for Foo<T> with no bounds on T.

One further consideration: a negative impl impl !Send for Foo isn't just "this is not Send", it's a promise that it won't become Send in the future. If a user wants "this is not Send but I'm making no promises that it won't be in the future", we would need something like impl ?Send for Foo. The lang team was generally in favor of adding this form as well.

To add to the @joshtriplett's comment:

  1. The "always applicable" rule is the same as we currently have for Drop
  2. The rule that is easy to remember with negative impls is: it's always a breaking change to remove an implementation, both positive and negative ones
    • This rule would be somewhat more complex with "questionable implementations" if we end up adding them, since then it would be okay to remove those

I've also volunteered to work on stabilization of this feature and related work here.

Are you proposing to also stabilize negative bounds (as a semver promise) for non-auto-traits? If not, how can removing a negative impl of an auto trait be a breaking change with any auto trait we have?

More on "questionable" impls:

  • We've wanted these in the past for other reasons, and indeed have #[rustc_reservation_impl] (https://github.com/rust-lang/rust/pull/62661/files#diff-3f4f29f2f5d7a54d28195beb05a24a0736a64ffe35eb30d8e7b2227c6a2fd615R564) to make this possible in certain cases.

  • I would love to have them in rustdocs for various things. If we can't yet agree on f32: !Ord, having a reserved impl Ord for f32; as a place to put "Look, it's intentional that this doesn't exist and here's what you should do instead".

  • Similarly, that could replace a bunch of the on_implemented use cases. reserved impl FromResidual<Option<_>> for Result<_, _>; would be nicer than doing that with text, for example, and could hopefully let it use the real trait solver instead of a side system with its own quirks.

  • They would also be great for new standard library traits, since they could start by always reserving impls for all our fundamental types. That would avoid problems like how we couldn't impl Extend for &mut impl Extend because of reference being fundamental, even though that would be useful to have.

  • With the tweaked coherence rules, IIRC you might sometimes want in a library to reserve some things (particularly blanket impls) to be able to have the option of adding them later, since otherwise downstream crates might add conflicting impls on their own types.

@ChayimFriedman2 no, negative bounds are a separate feature and there is no suggestions to stabilize that.

The reason removing a negative impl is a breaking change is that typechecker is allowed to assume that it is. It currently doesn't use this AFAIK, but in the feature we want to allow code like this:

struct NeverSend;
impl !Send for NeverSend {}

trait X {}

impl<T: Send> X for T {}
impl X for NeverSend {} // typechecked is allowed to assume those impl don't overlap

@WaffleLapkin we were working on this with @lcnr and @nikomatsakis and at some point but the work was deferred. I'm happy if you can move this forward and I'd be interested in following along. I think I even have a couple of PRs about this half-baked that I'm not sure if are relevant anymore :). Feel free to reach out if I can help with things.

There's also the RFC draft that we were working in meetings with Niko https://hackmd.io/ZmpF0ITPRWKx6jYxgCWS7g

@spastorino How easily could we add impl ?Send for SomeType?

lcnr commented

from a t-types pov it's pretty straightforward to implement with the following design constraints

  • only allowed for auto traits
  • required to be fully applicable, same as Drop

if t-lang decides that we should have that feature, it could be stabilized in 1-2 releases (assuming someone has the capacity to work)

@lcnr πŸ‘, my understanding was also that this is "easy" to do. I'd be up for working on this impl ?Send for SomeType if t-lang decides to have the feature.

  • Should we permit combining default + negative impls like default impl !Trait for Type { }? (Context)

One possibility would be to allow such impls to be specialized by a more specific positive impl defined in the same crate. This would provide a way of expressing that a positive impl is guaranteed not to be extended or generalized in the future.

@rustbot labels -I-lang-nominated

We discussed this, as mentioned above, and the team was in favor of doing it. The next step would be someone putting together a concrete proposal, probably in the form of a stabilization PR with a suitably detailed stabilization report.

Until then, there's probably not much more to discuss, so we'll remove the nomination. As soon as the stabilization report / PR is posted, please nominate for T-lang.

Since I found myself running into this, I wanted to poke around and see what's left of this feature. A few things that I wanted to clarify about the current status quo:

  1. Is there a genuine difference between impl !Trait and impl ?Trait besides just API guarantees? Essentially wondering whether more would need to be done than syntaxβ€” I'm assuming so, but it's not exactly clear.

    Yes, it affects coherence per #68318 (comment).

  2. Does the interaction with specialization have to be left as an unresolved issue here, or can it be moved into the tracking issue(s) for specialization instead? It feels silly to block stabilising this on how it interacts with an unstable feature, unless there's serious concern that stabilising it now could break that in the future. (I don't think it would, but worth checking.)

    Discussed here: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20specialization/near/496281282

  3. The implication here is that the lang team is okay with just a stabilisation report and an FCP for this, but since there was also a linked draft RFC, it's worth checking whether the request is to make a stabilisation report or an actual RFC for this.

    Yes, an RFC is necessary per #68318 (comment).

  4. Is there any consensus on how negative impls should interact with unsafe traits? For example, if you have unsafe trait Trait, would you have to do unsafe impl !Trait or just impl !Trait? My assumption is that the negative impl would not have to be unsafe, but this is something that wasn't mentioned at all from what I read.

    Discussed here: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20unsafe.20traits/near/496281455

  5. When it comes to stabilising this feature, should conditional negative impls be put under a separate feature gate, or just removed entirely for now? Not sure what the consensus is here.Since it's been over a year since folks have posted here, I'm assuming that nobody is currently busy with this, and I would like to help at least work on cleaning up the details here, but I also am fine not doing so if someone else is still working on it.

    It's okay to just outright remove these entirely, although it would be fine to also move them to a separate feature gate, per #68318 (comment).


Some additional questions to answer, added after the fact:

  1. Is impl ?Trait considered a hard requirement of stabilisation, or is it something that's simply desired as a future extension?

    It's not a hard requirement and can be a future extension, per #68318 (comment).

  2. Is !Drop a valid impl? How does this interact with Destruct?

    Discussed here: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20.60!Drop.60

  1. Is there a genuine difference between impl !Trait and impl ?Trait besides just API guarantees? Essentially wondering whether more would need to be done than syntaxβ€” I'm assuming so, but it's not exactly clear.

Yes. impl !Unpin can be relied upon in coherence checking, but ?Unpin cannot.

For the rest of these good questions, we should probably discuss:

@rustbot labels +I-lang-nominated

Also:

cc @rust-lang/types

2. Does the interaction with specialization have to be left as an unresolved issue here, or can it be moved into the tracking issue(s) for specialization instead? It feels silly to block stabilising this on how it interacts with an unstable feature, unless there's serious concern that stabilising it now could break that in the future.

async discussion thread: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20specialization/near/496281282

4. Is there any consensus on how negative impls should interact with unsafe traits? For example, if you have unsafe trait Trait, would you have to do unsafe impl !Trait or just impl !Trait? My assumption is that the negative impl would not have to be unsafe, but this is something that wasn't mentioned at all from what I read.

async discussion thread: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20unsafe.20traits/near/496281455

I think we should just rule out !Drop -- drop is a very special trait.

I found a use case for this while working on async-local; for types that don't impl Drop, the thread_local macro won't register destructor functions, and the lifetime of these types can be extended by using Condvar making it possible to hold references to thread locals owned by runtime worker threads in an async context or on any runtime managed thread so long as worker threads rendezvous while destroying thread local data. For types that do impl Drop, they will immediately deallocate regardless of whether the owning thread blocks and so synchronized shutdowns cannot mitigate the possibility of pointers being invalidated, making the safety of this dependent on types not implementing Drop.

@Bajix This sounds like a hack around lifetimes. Is there any guarantee that accessing a !Drop value held by a thread is valid simply because the thread hasn't exited yet?

I'm asking because, once specialization is available, it may make sense to let every type implement Drop. Semantically this is simpler and theoretically it should be no different (note that Rust does not guarantee that drop will be called on thread exit).

Note that β€œdoes not implement Drop” does not mean β€œhas a trivial destructor”, if the type has a Drop-implementing field.

It may make sense to let every type implement Drop.

I agree, but I don't think specialization is necessarily required. I propose this at rust-lang/rfcs#3762 (comment)

Please start a thread for !Drop, it seems to require some discussion and tracking issues are not very well suited for that

Please start a thread for !Drop, it seems to require some discussion and tracking issues are not very well suited for that

https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/negative.20impls.3A.20.60!Drop.60/near/496452885

Also, FWIW, I'm treating my earlier comment as a list of current questions, so, any team members are free to edit it to update the list if necessary: #68318 (comment)

Or just move it to the issue description!

@rustbot labels -I-lang-nominated

In lang triage today we reviewed this and answered the three questions outstanding for us:

  1. The implication here is that the lang team is okay with just a stabilisation report and an FCP for this, but since there was also a linked draft RFC, it's worth checking whether the request is to make a stabilisation report or an actual RFC for this.

We want to see an RFC for this. There is enough to document here that we felt this warranted.

  1. When it comes to stabilising this feature, should conditional negative impls be put under a separate feature gate, or just removed entirely for now? Not sure what the consensus is here.Since it's been over a year since folks have posted here, I'm assuming that nobody is currently busy with this, and I would like to help at least work on cleaning up the details here, but I also am fine not doing so if someone else is still working on it.

Our feeling was that it was fine to just remove conditional negative impls for now. Though this is somewhat of an implementation question; I doubt we'd object as a lang matter to these staying around under a separate feature gate.

  1. Is impl ?Trait considered a hard requirement of stabilisation, or is it something that's simply desired as a future extension?

We didn't see this as a hard requirement and felt it was fine to treat this as a future extension.

Thank you for clarifying everything! This issue should probably be tagged as needs-rfc then, assuming I understand the issue tags correctly.

I'm working on splitting out negative impls into a separate feature gate (arbitrary_negative_impls), and just wanted to note that it's not possible to remove conditional negative impls from the standard library since we rely on them for coherence.

since we rely on them for coherence.

Where? Could you please post a link?

lcnr commented

after a quick chat with @compiler-errors I would like to understand the "no conditional impls" restriction outside of disabling auto trait implementations.

I do not believe negative impls are the correct feature to disable builtin auto trait impls because negative impls provide an API guarantee which is most likely not what people actually intend to do.

Instead of adding impl ?Send for MyType which is confusing, has a builtin-attribute based approach been considered:

#[disable_builtin_impl(Send)]
struct ThisIsNotSend(u32);

In our lang discussions, I gather that we all agreed that negative impls are not the correct way to disable those impls in general, exactly because of the API guarantee you mention. That's why we are interested in seeing some way to do that exist eventually (other than adding e.g. PhantomNotSend fields). As far as I recall, we haven't talked about using an attribute rather than impl ?Tr for .. syntax, but it's harder to see how that would work cleanly for one of the cases that @nikomatsakis mentioned:

// A crate is at v1:
trait Tr {}

// An external crate now does:
struct LocalTy;
impl Tr for Rc<LocalTy> {}

// Later, the original crate wants to add:
impl<T> Tr for T {} //~ Breaking change.

So, what we may want so that the trait's crate can hold space is:

impl<T> ?Tr for T {} //~ Proposed feature.

We discussed how you could almost get this with a blanket impl with an alway-ambiguous WC bound, except that you'd have to impl the trait items.

@nikomatsakis also mentioned:

NM: thinking after: Another motivation I had for ? impls was to avoid excessive inference...

impl AsRef<str> for MyType { } // if you just have this impl, `MyType: AsRef<?X>` might infer that `?X = str`
impl<T> ?AsRef<!> for MyType { } // may add this in the future, don't infer that `?X = str`, here `!` is just playing the role of "some type"

...though I might prefer to have other ways to say this.

I clarified with @nikomatsakis and wanted to state that I've decided that imposing the "always applicable" restriction for all negative trait impls is a bit arbitrary, but it is still useful for auto traits, since like positive impls, this probably doesn't mean the thing you want it to mean:

struct W<T>(T);

impl !Send for W<i32> {}

Namely, that negative impl also opts-out the built-in positive impl of Send for W<T> even for types like W<i32>. See #93367 for why (I believe this is the right issue).

But for non-auto traits, having "partial" negative impls is actually pretty useful. Or at least it makes no sense to restrict the generality of the feature.

I'll put up a feature that checks that negative impls of auto traits are always applicable.