rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.89k stars 12.52k forks source link

Tracking issue for promoting `!` to a type (RFC 1216) #35121

Open nikomatsakis opened 8 years ago

nikomatsakis commented 8 years ago

Tracking issue for rust-lang/rfcs#1216, which promotes ! to a type.

About tracking issues

Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.

Pending issues to resolve

Interesting events and links

canndrew commented 7 years ago

Regarding the uninitialized/transmute/MaybeUninitialized issue I think a sensible course of action would be:

Thoughts?

nikomatsakis commented 7 years ago

See this thread on the recent changes to exhaustiveness:

arielb1 commented 7 years ago

There appears to be some disagreement about the semantics of &!. After #39151, with the never_type feature gate enabled it is treated as uninhabited in matches.

Kixunil commented 7 years ago

If automatic trait impls for ! won't be there, at least implementing appropriate traits from std would be very helpful. One huge limitation of void::Void is that it lives outside of std and that means blanket impl<T> From<Void> for T is impossible and that limits error handling.

I think at least From<!> should be implemented for all types and Error, Display and Debug should be implemented for !.

glaebhoerl commented 7 years ago

I think at least From<!> should be implemented for all types

This unfortunately conflicts with the From<T> for T impl.

canndrew commented 7 years ago

This unfortunately conflicts with the From<T> for T impl.

At least until lattice impl specialization is implemented.

Kixunil commented 7 years ago

Good points. I hope to see it done one day.

nagisa commented 7 years ago

Should [T; 0] subtype a [!; 0] and &[T] subtype a &[!]? It seems intuitive to me that the answer ought to be “yes”, but the current implementation thinks otherwise.

eddyb commented 7 years ago

[!; 0] is inhabited and so is &[!] so I'd say no. Besides, we're not using subtyping this time around.

tbu- commented 7 years ago

Both [!; 0] and &[!] shouldn't be uninhabited, both types can accept the value [] (or &[]).

nagisa commented 7 years ago

Nobody said they are or should be uninhabited.

jsgf commented 7 years ago

let _: u32 = unsafe { mem::transmute(return) }; In principle this could be OK, but the compiler complains "transmuting between 0 bits and 32 bits".

Transmuting ! -> () is allowed however. I don't have any particular reason to point this out; it's an inconsistency, but I can't think of a practical or theoretical problem with it.

nikomatsakis commented 7 years ago

I would not expect subtyping and would be opposed to it on the grounds of avoiding complicating all manner of logic in the compiler. I do not want subtyping that is unrelated to regions or region binding, basically.

oberien commented 7 years ago

Is it possible to implement Fn, FnMut and FnOnce for ! in a way that it is generic over all input and output types?

In my case I have a generic builder which can take a function, which will be invoked when building:

struct Builder<F: FnOnce(Input) -> Output> {
    func: Option<F>,
}

As func is an Option, a constructor using None can't infer the type of F. Therefore a fn new implementation should use Bang:

impl Builder<!> {
    pub fn new() -> Builder<!> {
        Builder {
            func: None,
        }
    }
}

The builder's func function should look something like this to be invoked both on Builder<!> and Builder<F>:

impl<F: FnOnce(Input) -> Output> Builder<F> {
    pub fn func<F2: FnOnce(Input) -> Output>(self, func: F) -> Builder<F2> {
        Builder {
            func: func,
        }
    }
}

Now the Problem: Currently the Fn* traits are not implemented for !. Additionally, you can't have a generic associated type for traits (e.g. Output in Fn). So is it possible to implement the Fn* trait family for ! in a way that it is generic over all input and output types nevertheless?

SimonSapin commented 7 years ago

That sounds contradictory. Doesn’t <! as Fn>::Output need to resolve to a single type?

taralx commented 7 years ago

<! as Fn>::Output is !, isn't it?

oberien commented 7 years ago

In theory <! as Fn>::Output should be !, but the current type checker wants associated types to match exactly: https://is.gd/4Mkxfm

@SimonSapin It does need to resolve to a single type, which is why I raise my question. From a language point of view it's completely correct behaviour. But from a library point of view the usage of ! in contexts of generic functions would be very limited if the current behaviour is kept.

earthengine commented 7 years ago

Should the following code

#![feature(never_type)]

fn with_print<T>(i:i32, r:T) -> T {
    println!("{}", i);
    r
}

fn main() {
    #[allow(unreachable_code)]
    *with_print(10,&return)
}

prints 10? Right now it does not print anything. Or any other workarounds that will delay the return until the print?

I mean, as I said before, there is actually two different kinds of ! type: one is lazy and one is eager. The usual notation denotes an eager one, but sometime we might need the lazy one.


As @RalfJung is interested in a previous version that uses &return, I adjusted the code again. And again, My point is we should be able to delay the return later. We could use closures here but I can only use return, not break or continue etc.

For example, it would be good if we could write

#![feature(never_type)]

fn with_print<T>(i:i32, r:T) -> T {
    println!("{}", i);
    r
}

fn main() {
    for i in 1..10 {
        if i==5 { with_print(i, /* delay */break) }
        with_print(i, i);
    }
}

with the break delayed until 5 being printed.

cramertj commented 7 years ago

@earthengine No-- that should most definitely not print anything. ! is uninhabited, which means that no values of type ! can be created. So if you have a reference to a value of type !, you're inside of a dead code block.

The reason that return has type ! is because it diverges. Code that uses the result of a return expression can never be executed because return never "finishes".

RalfJung commented 7 years ago

oO I did not know you can write &return. That makes no sense, why should you be allowed to take the address of return?^^

But, anyway, @earthengine in your code, arguments are evaluated before quit_with_print is called. Evaluating the 2nd argument runs return, which terminates the program. This is like writing something like foo({ return; 2 }) -- foo is never executed.

cramertj commented 7 years ago

@RalfJung return is an expression of type !, just like break or continue. It has a type, but its type is uninhabited.

canndrew commented 7 years ago

! has been implemented and working in nightly for a year now. I'd like to know what needs to be done to move it towards stabilization.

One thing that hasn't been resolved is what to do about the intrinsics that can return ! (ie. uninitialized, ptr::read and transmute). For uninitialized, last I heard there was a consensus that it should be deprecated in favor of a MaybeUninit type and possible future &in, &out, &uninit references. There's no RFC for this, although there is an earlier RFC where I proposed deprecating uninitialized just for types that don't implement a new Inhabited trait. Perhaps that RFC should be scrapped and replaced with "deprecate uninitialized" and "add MaybeUninit" RFCs?

For ptr::read, I think it's fine to leave it as UB. When someone calls ptr::read they're asserting that the data they're reading is valid, and in the case of ! it's definitely not. Maybe someone has a more nuanced opinion on this though?

Fixing transmute is easy - just make it an error to transmute to an uninhabited type (instead of ICEing as it currently does). There was a PR to fix this but it was closed with the reason that we still need a better idea of how to treat uninitialized data.

So where are we at with these at the moment? (/cc @nikomatsakis)? Would we be ready to move forward with deprecating uninitialized and adding a MaybeUninit type? If we made these intrinsics panic when called with !, would that be a suitable stop-gap measure which would allow us to stabilise !?

There's also the pending issues listed:

What traits should we implement for !? The initial PR #35162 includes Ord and a few others. This is probably more of a T-libs issue, so I'm adding that tag to the issue.

Currently there's a fairly basic selection: PartialEq, Eq, PartialOrd, Ord, Debug, Display, Error. Besides Clone, which should definitely be added to that list, I can't see any others that are vitally important. Do we need to block stabilization on this? We could just add more impls later if we see fit.

How to implement warnings for people relying on (): Trait fallback where that behavior might change in the future?

The resolve_trait_on_defaulted_unit warning is implemented and already in stable.

Desired semantics for ! in coercion (#40800) Which type variables should fallback to ! (#40801)

These seem like the real blockers. @nikomatsakis: Do you have any concrete ideas about what to do about these that are just waiting to be implemented?

jsgf commented 7 years ago

Does it make sense for ! to also implement Sync and Send? There are several cases where error types have Sync and/or Send bounds, where it would be useful to use ! as "can't fail".

Kixunil commented 7 years ago

@canndrew I disagree with forbidding it in unsafe code. The unreachable crate uses transmute to create uninhabited type and so hint optimizer that certain branch of code can never occur. This is useful for optimizations. I plan to make my own crate using such technique in interesting way.

RalfJung commented 7 years ago

@Kixunil wouldn't it be more explicit to use https://doc.rust-lang.org/beta/std/intrinsics/fn.unreachable.html here?

Kixunil commented 7 years ago

@RalfJung it would, but that is unstable. Reminds me that forbidding transmuting into uninhabited types would be a breaking change.

canndrew commented 7 years ago

@jsgf Sorry, yes, ! also impls all the marker traits (Send, Sync, Copy, Sized).

@Kixunil Hmm, that's a shame. It would be much better to stabilize the unreachable intrinsic though.

scottmcm commented 7 years ago

Not a stabilization-blocker (because perf, not semantics), but it seems that Result<T, !> isn't using the uninhabitedness of ! in enum layout or match codegen: https://github.com/rust-lang/rust/issues/43278

ExpHP commented 7 years ago

This is such a vague idea that I almost feel guilty for dropping it here, but:

Could chalk possibly help answer some of the tougher questions with regards to !: Trait implementations?

canndrew commented 7 years ago

@ExpHP I don't think so. Auto-implementing !: Trait for traits which don't have associated types/consts and only non-static methods is known to be sound. It's just a matter of whether we want to do it or not. Auto-implementing for other traits doesn't really make sense because the compiler would need to insert arbitrary values for the associated types/consts and invent it's own arbitrary static method impls.

lambda-fairy commented 7 years ago

There is already a resolve-trait-on-defaulted-unit lint in rustc. Should the corresponding item be ticked off?

taralx commented 7 years ago

@canndrew Is it unsound to set associated types to !?

lambda-fairy commented 7 years ago

@taralx it's not unsound, but it would prevent valid code from working. Note that the following code compiles today:

trait Foo {
    type Bar;
    fn make_bar() -> Self::Bar;
}

impl Foo for ! {
    type Bar = (i32, bool);
    fn make_bar() -> Self::Bar { (42, true) }
}
earthengine commented 7 years ago

@lfairy It depends on your opinion on what is "valid" code. For me, I would happily accept that the code you given to be "invalid", as you should not be able to get anything from !.

lambda-fairy commented 7 years ago

@earthengine I don't see the point you're trying to make. You can call a static method on a type without having an instance of it. Whether or not you can "get anything" from ! has nothing to do with that.

canndrew commented 7 years ago

There's no reason why you shouldn't be able to get anything from ! the type. You can't get anything from a ! though, which is why non-static methods on ! could be derived without forcing any decisions on the programmer.

Ericson2314 commented 7 years ago

I think the easiest to remember rule would be implementing any trait for which there there is exactly 1 valid implementation (excluding those that add more divergence than !s in scope already cause).

Anything we go with should be equal or more conservative with that (which no automatic impls also fits).

SimonSapin commented 7 years ago

@Ericson2314 I don’t understand what that means, could you give some examples?

Ericson2314 commented 7 years ago

@SimonSapin The "no more divergence" rule means no panic!() or loop { }, but a v: ! that is already in scope is fine. With expressions like those ruled out, many traits only have one possible impl for ! that type-checks. (Others may have an informal contract which rules out all but 1 impl, but we can't deal with those automatically.)

// two implementations: constant functions returning true and false.
// And infinitely more with side effects taken into account.
trait Foo { fn() -> bool }
// Exactly one implementation because the body is unreachable no matter what.
trait Bar { fn(Self) -> Self }
RalfJung commented 7 years ago

Mathematically speaking, ! is an "initial element", which means that for a type signature that has ! in its arguments, there is exactly one implementation, i.e., all implementations are equal -- when observed from the outside. This is because they all cannot be called.

arielb1 commented 7 years ago

What's actually blocking this from stabilization? Is it only the mem::uninitialized situation or something else?

canndrew commented 7 years ago

@arielb1: I think it's #40800 and #40801 aswell. Do you know what the status is on these?

QuietMisdreavus commented 7 years ago

What docs have been written on this, or are people planning to write for this? With the recent additions to the primitive type pages in the std docs (#43529, #43560), should the ! type get a page there? The reference should probably also get updated, if it hasn't already.

Eroc33 commented 7 years ago

Are there any plans to allow specifiying that asm is diverging so we can write things like?

#[naked]
unsafe fn error() -> !{
  asm!("hlt");
}

Without having to use loop{} to appease the type checker?

Edit: Although I suppose using core::intrinsics::unreachable may be acceptable in very low level code.

Kixunil commented 7 years ago

@Eroc33 when ! is a type, you can do this:

#[naked]
unsafe fn error() -> ! {
  asm!("hlt");
  std::mem::uninitialized()
}

Until then you can do this:

#[naked]
unsafe fn error() -> ! {
  asm!("hlt");

  enum Never {}
  let never: Never = std::mem::uninitialized();
  match never {}
}
eddyb commented 7 years ago

@Kixunil ! is a type on nightly which you need to use asm!. This is usually done as:

#[naked]
unsafe fn error() -> ! {
    asm!("hlt");
    std::intrinsics::unreachable();
}
tbu- commented 7 years ago

@Kixunil It can be done more easily using std::intrinics::unreachable() which exactly specifies that the code before is diverging.

Eroc33 commented 7 years ago

@Kixunil From the above discussion std::mem::uninitialized being able to return ! appears to be subject to change though? Unless I have misunderstood that?

canndrew commented 7 years ago

From the above discussion std::mem::uninitialized being able to return ! appears to be subject to change though? Unless I have misunderstood that?

I think uninitialized will eventually be deprecated entirely and, as a stop gap, will be made to panic at runtime when used with ! (though that wouldn't effect this case).

I also think we'll need to stabilize std::intrinsics::unreachable somewhere in libcore and name it unchecked_unreachable or something to distinguish it from the unreachable! macro. I've been meaning to write an RFC for that.

Kixunil commented 7 years ago

@eddyb Ah, yeah, forgot that asm!() is unstable, so std::intrinsics::unreachable() might be used as well.

@tbu- Sure, I highly prefer that one instead of creating uninhabited type. The issue is, it's unstable, so if it was somehow impossible to unsafely mark branch of code as unreachable, it'd not only break existing code, but also make it unfixable. I think such thing would severely harm Rust reputation (especially considering main developers proudly claiming it being stable). As much as I love Rust, such thing would make me reconsider it being useful language.

@Eroc33 my understanding is that some people suggest doing it, but it's mere suggestion, not definite decision. And I stand against such suggestion because it'd break backward compatibility, forcing Rust to become 2.0. I don't think there is any harm supporting it. If people are concerned about bugs, lint would be more appropriate.

@canndrew Why do you think uninitialized will be deprecated? I can't believe such thing. I find it extremely useful, so unless there exists a very good replacement, I don't see any reason for doing so.

Finally, I'd like to reiterate that I agree that intrinsics::unreachable() is better than current hacks. That being said, I oppose forbidding or even deprecating those hacks until there is a sufficient replacement.