Open nikomatsakis opened 8 years ago
Regarding the uninitialized
/transmute
/MaybeUninitialized
issue I think a sensible course of action would be:
MaybeUninitialized
to the standard library, under mem
uninitialized
that it's type is known to be inhabited. Raise a warning otherwise with a note saying that this will be a hard error in the future. Suggest using MaybeUninitialized
instead.transmute
that it's "to" type is only uninhabited if it's "from" type is aswell. Similarly raise a warning which will become a hard error.Thoughts?
See this thread on the recent changes to exhaustiveness:
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.
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 !
.
I think at least
From<!>
should be implemented for all types
This unfortunately conflicts with the From<T> for T
impl.
This unfortunately conflicts with the
From<T> for T
impl.
At least until lattice impl specialization is implemented.
Good points. I hope to see it done one day.
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.
[!; 0]
is inhabited and so is &[!]
so I'd say no. Besides, we're not using subtyping this time around.
Both [!; 0]
and &[!]
shouldn't be uninhabited, both types can accept the value []
(or &[]
).
Nobody said they are or should be uninhabited.
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.
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.
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?
That sounds contradictory. Doesn’t <! as Fn>::Output
need to resolve to a single type?
<! as Fn>::Output
is !
, isn't it?
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.
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.
@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".
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.
@RalfJung return
is an expression of type !
, just like break
or continue
. It has a type, but its type is uninhabited.
!
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 includesOrd
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?
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".
@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.
@Kixunil wouldn't it be more explicit to use https://doc.rust-lang.org/beta/std/intrinsics/fn.unreachable.html here?
@RalfJung it would, but that is unstable. Reminds me that forbidding transmuting into uninhabited types would be a breaking change.
@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.
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
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?
@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.
There is already a resolve-trait-on-defaulted-unit
lint in rustc. Should the corresponding item be ticked off?
@canndrew Is it unsound to set associated types to !
?
@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) }
}
@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 !
.
@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.
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.
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).
@Ericson2314 I don’t understand what that means, could you give some examples?
@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 }
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.
What's actually blocking this from stabilization? Is it only the mem::uninitialized
situation or something else?
@arielb1: I think it's #40800 and #40801 aswell. Do you know what the status is on these?
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.
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.
@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 {}
}
@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();
}
@Kixunil It can be done more easily using std::intrinics::unreachable()
which exactly specifies that the code before is diverging.
@Kixunil From the above discussion std::mem::uninitialized
being able to return !
appears to be subject to change though? Unless I have misunderstood that?
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.
@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.
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
()
https://github.com/rust-lang/rust/issues/67225Interesting events and links