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

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

Open
nikomatsakis opened this Issue Jul 29, 2016 · 245 comments

Comments

@nikomatsakis
Contributor

nikomatsakis commented Jul 29, 2016

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

Pending issues to resolve:

  • 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.
  • Remove coercion into type ! #35121
  • How to implement warnings for people relying on (): Trait fallback where that behavior might change in the future?
  • Desired semantics for ! in coercion (#40800)
  • Which type variables should fallback to ! (#40801)
  • Code cleanup from #35162, re-organize typeck a bit to thread types through return position instead of calling expr_ty
  • Resolve treatment of uninhabited types in matches
  • Resolve ICE with *const ! - #43061
  • #47563 -- [!; 0] not uninhabited

Related:

  • #46164: we could do a better job linting questionable fallback behavior
  • #46325 -- future compat warning for coercion to !

Interesting events:

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Jul 30, 2016

Contributor

Huzzah!

There's a WIP implementation of this here: https://github.com/canndrew/rust/tree/bang_type_coerced

It's current status is: it builds with old-trans and is usable but has a couple of failing tests. Some tests fail due to a bug that causes code like if (return) {} to crash during trans. The other tests are to do with link-time-optimization and have always been buggy for me, so I don't know if they have anything to do with my changes.

My current roadmap is:

  • Get it working with MIR. This hopefully won't be too hard as this is how I started implementing it, but I've been having a problem where MIR builds segfault during compilation.
  • Purge the obsolete divergence stuff from the compiler (FnOutput, FnDiverging and related).
  • Hide the new type behind a feature gate. This would mean, when the feature is disabled:
    • ! can only be parsed as a type in the return position.
    • Diverging type variables default to ().
  • Figure out how we can raise compatibility warnings when a defaulted () gets used to resolve a trait. One way to do this could be to add a new type to the AST called DefaultedUnit. This type behaves like () and will turn into () under some circumstances but raises a warning when it resolves a trait (as ()). The problem with this approach is I think it will be hard to catch and fix all the bugs with the implementation - we'd end up breaking people's code in order to save their code from being broken.

Is there anything that needs to be added to this list? Is it just going to be me working on this? And should this branch be moved onto the main repository?

Contributor

canndrew commented Jul 30, 2016

Huzzah!

There's a WIP implementation of this here: https://github.com/canndrew/rust/tree/bang_type_coerced

It's current status is: it builds with old-trans and is usable but has a couple of failing tests. Some tests fail due to a bug that causes code like if (return) {} to crash during trans. The other tests are to do with link-time-optimization and have always been buggy for me, so I don't know if they have anything to do with my changes.

My current roadmap is:

  • Get it working with MIR. This hopefully won't be too hard as this is how I started implementing it, but I've been having a problem where MIR builds segfault during compilation.
  • Purge the obsolete divergence stuff from the compiler (FnOutput, FnDiverging and related).
  • Hide the new type behind a feature gate. This would mean, when the feature is disabled:
    • ! can only be parsed as a type in the return position.
    • Diverging type variables default to ().
  • Figure out how we can raise compatibility warnings when a defaulted () gets used to resolve a trait. One way to do this could be to add a new type to the AST called DefaultedUnit. This type behaves like () and will turn into () under some circumstances but raises a warning when it resolves a trait (as ()). The problem with this approach is I think it will be hard to catch and fix all the bugs with the implementation - we'd end up breaking people's code in order to save their code from being broken.

Is there anything that needs to be added to this list? Is it just going to be me working on this? And should this branch be moved onto the main repository?

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 1, 2016

Contributor

Figure out how we can raise compatibility warnings when a defaulted () gets used to resolve a trait. One way to do this could be to add a new type to the AST called DefaultedUnit. This type behaves like () and will turn into () under some circumstances but raises a warning when it resolves a trait (as ()). The problem with this approach is I think it will be hard to catch and fix all the bugs with the implementation - we'd end up breaking people's code in order to save their code from being broken.

@eddyb, @arielb1, @anyone_else: Thoughts on this approach? I'm pretty much up to this stage (sans a couple of failing tests that I'm (very slowly) trying to fix).

Contributor

canndrew commented Aug 1, 2016

Figure out how we can raise compatibility warnings when a defaulted () gets used to resolve a trait. One way to do this could be to add a new type to the AST called DefaultedUnit. This type behaves like () and will turn into () under some circumstances but raises a warning when it resolves a trait (as ()). The problem with this approach is I think it will be hard to catch and fix all the bugs with the implementation - we'd end up breaking people's code in order to save their code from being broken.

@eddyb, @arielb1, @anyone_else: Thoughts on this approach? I'm pretty much up to this stage (sans a couple of failing tests that I'm (very slowly) trying to fix).

dns2utf8 added a commit to dns2utf8/rust that referenced this issue Aug 4, 2016

dns2utf8 added a commit to dns2utf8/rust that referenced this issue Aug 4, 2016

@nikomatsakis nikomatsakis added the T-libs label Aug 10, 2016

@tomaka

This comment has been minimized.

Show comment
Hide comment
@tomaka

tomaka Aug 11, 2016

Contributor

What traits should we implement for !? The initial PR #35162 includes Ord and a few others.

Shouldn't ! automatically implement all traits?

This kind of code is fairly common:

trait Baz { ... }

trait Foo {
    type Bar: Baz;

    fn do_something(&self) -> Self::Bar;
}

I'd expect ! to be usable for Foo::Bar in order to indicate that a Bar can never actually exist:

impl Foo for MyStruct {
    type Bar = !;
    fn do_something(&self) -> ! { panic!() }
}

But this is only possible if ! implements all traits.

Contributor

tomaka commented Aug 11, 2016

What traits should we implement for !? The initial PR #35162 includes Ord and a few others.

Shouldn't ! automatically implement all traits?

This kind of code is fairly common:

trait Baz { ... }

trait Foo {
    type Bar: Baz;

    fn do_something(&self) -> Self::Bar;
}

I'd expect ! to be usable for Foo::Bar in order to indicate that a Bar can never actually exist:

impl Foo for MyStruct {
    type Bar = !;
    fn do_something(&self) -> ! { panic!() }
}

But this is only possible if ! implements all traits.

@suhr

This comment has been minimized.

Show comment
Hide comment
@suhr

suhr Aug 11, 2016

@tomaka There's RFC about it: rust-lang/rfcs#1637

The problem is that if ! implements Trait it also should implement !Trait...

suhr commented Aug 11, 2016

@tomaka There's RFC about it: rust-lang/rfcs#1637

The problem is that if ! implements Trait it also should implement !Trait...

@tomaka

This comment has been minimized.

Show comment
Hide comment
@tomaka

tomaka Aug 11, 2016

Contributor

The problem is that if ! implements Trait it also should implement !Trait...

Then special-case ! so that it ignores any trait requirement?

Contributor

tomaka commented Aug 11, 2016

The problem is that if ! implements Trait it also should implement !Trait...

Then special-case ! so that it ignores any trait requirement?

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 11, 2016

Contributor

@tomaka ! can't automatically implement all traits because traits can have static methods and associated types/consts. It can automatically implement traits that just have non-static methods though (ie. methods that take a Self).

As for !Trait, someone suggested that ! could auto-implement both Trait and !Trait. I'm not sure whether that's sound but I suspect that negative traits aren't sound at all.

Contributor

canndrew commented Aug 11, 2016

@tomaka ! can't automatically implement all traits because traits can have static methods and associated types/consts. It can automatically implement traits that just have non-static methods though (ie. methods that take a Self).

As for !Trait, someone suggested that ! could auto-implement both Trait and !Trait. I'm not sure whether that's sound but I suspect that negative traits aren't sound at all.

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 11, 2016

Contributor

But yes, it might be nice if ! could auto-implement Baz in the example you gave for precisely these sorts of cases.

Contributor

canndrew commented Aug 11, 2016

But yes, it might be nice if ! could auto-implement Baz in the example you gave for precisely these sorts of cases.

dns2utf8 added a commit to dns2utf8/rust that referenced this issue Aug 15, 2016

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 19, 2016

Contributor

When exactly do we default diverging type variables to ()/! and when do we throw an error about not being able to infer enough type information? Is this specified anywhere? I'd like to be able to compile the following code:

let Ok(x) = Ok("hello");

But the first error I get is "unable to infer enough type information about _". In this case I think it would make sense for _ to default to !. However when I was writing tests around defaulting behaviour I found it surprisingly difficult to make a type variable default. That's why these tests are so convoluted.

I'd like to have a clear idea of exactly why we have this defaulting behaviour and when it's supposed to get invoked.

Contributor

canndrew commented Aug 19, 2016

When exactly do we default diverging type variables to ()/! and when do we throw an error about not being able to infer enough type information? Is this specified anywhere? I'd like to be able to compile the following code:

let Ok(x) = Ok("hello");

But the first error I get is "unable to infer enough type information about _". In this case I think it would make sense for _ to default to !. However when I was writing tests around defaulting behaviour I found it surprisingly difficult to make a type variable default. That's why these tests are so convoluted.

I'd like to have a clear idea of exactly why we have this defaulting behaviour and when it's supposed to get invoked.

@tomaka

This comment has been minimized.

Show comment
Hide comment
@tomaka

tomaka Aug 19, 2016

Contributor

But the first error I get is "unable to infer enough type information about _". In this case I think it would make sense for _ to default to !. However when I was writing tests around defaulting behaviour I found it surprisingly difficult to make a type variable default. That's why these tests are so convoluted.

That's a very good idea in my opinion. Same for None which would default to Option<!> for example.

Contributor

tomaka commented Aug 19, 2016

But the first error I get is "unable to infer enough type information about _". In this case I think it would make sense for _ to default to !. However when I was writing tests around defaulting behaviour I found it surprisingly difficult to make a type variable default. That's why these tests are so convoluted.

That's a very good idea in my opinion. Same for None which would default to Option<!> for example.

dns2utf8 added a commit to dns2utf8/rust that referenced this issue Aug 20, 2016

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Aug 20, 2016

Contributor

@carllerche

The unit_fallback test is certainly an odd way to demonstrate it. A less-macro-ey version is

trait Balls: Default {}
impl Balls for () {}

struct Flah;

impl Flah {
    fn flah<T: Balls>(&self) -> T {
        Default::default()
    }
}

fn doit(cond: bool) {
    let _ = if cond {
        Flah.flah()
    } else {
        return
    };
}

fn main() {
    let _ = doit(true);
}

Only the type variable created by return/break/panic!() defaults to anything.

Contributor

arielb1 commented Aug 20, 2016

@carllerche

The unit_fallback test is certainly an odd way to demonstrate it. A less-macro-ey version is

trait Balls: Default {}
impl Balls for () {}

struct Flah;

impl Flah {
    fn flah<T: Balls>(&self) -> T {
        Default::default()
    }
}

fn doit(cond: bool) {
    let _ = if cond {
        Flah.flah()
    } else {
        return
    };
}

fn main() {
    let _ = doit(true);
}

Only the type variable created by return/break/panic!() defaults to anything.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Aug 24, 2016

Contributor

When exactly do we default diverging type variables to ()/! and when do we throw an error about not being able to infer enough type information? Is this specified anywhere?

Define "specified". :) The answer is that certain operations, which are not afaik written down anywhere outside the code, require that the type is known at that point. The most common case is field access (.f) and method dispatch (.f()), but another example is deref (*x), and there is probably one or two more. There are mostly decent reasons for this being required -- generally speaking, there are multiple diverging ways to proceed, and we can't make progress without knowing which one to take. (It would be possible, potentially, to refactor the code so that this need can be registered as a kind of "pending obligation", but it's complicated to do so.)

If you make it all the way to the end of the fn, then we run all pending trait selection operations until a steady state is reached. This is the point where defaults (e.g., i32, etc) are applied. This last part is described in the RFC talking about user-specified default type parameters (though that RFC in general needs work).

Contributor

nikomatsakis commented Aug 24, 2016

When exactly do we default diverging type variables to ()/! and when do we throw an error about not being able to infer enough type information? Is this specified anywhere?

Define "specified". :) The answer is that certain operations, which are not afaik written down anywhere outside the code, require that the type is known at that point. The most common case is field access (.f) and method dispatch (.f()), but another example is deref (*x), and there is probably one or two more. There are mostly decent reasons for this being required -- generally speaking, there are multiple diverging ways to proceed, and we can't make progress without knowing which one to take. (It would be possible, potentially, to refactor the code so that this need can be registered as a kind of "pending obligation", but it's complicated to do so.)

If you make it all the way to the end of the fn, then we run all pending trait selection operations until a steady state is reached. This is the point where defaults (e.g., i32, etc) are applied. This last part is described in the RFC talking about user-specified default type parameters (though that RFC in general needs work).

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 27, 2016

Contributor

#36011
#36038
#35940

Are some bugs to add to the list of pending issues.

Contributor

canndrew commented Aug 27, 2016

#36011
#36038
#35940

Are some bugs to add to the list of pending issues.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Aug 27, 2016

Contributor

@canndrew those look a bit similar to #12609

Contributor

glaebhoerl commented Aug 27, 2016

@canndrew those look a bit similar to #12609

@canndrew

This comment has been minimized.

Show comment
Hide comment
@canndrew

canndrew Aug 27, 2016

Contributor

Boy, that's an old bug! But yes I'd say my #36038 is a dupe of that (I thought I'd seen it somewhere before). I don't think ! can really be considered for prime time until that's fixed.

Contributor

canndrew commented Aug 27, 2016

Boy, that's an old bug! But yes I'd say my #36038 is a dupe of that (I thought I'd seen it somewhere before). I don't think ! can really be considered for prime time until that's fixed.

@tikue

This comment has been minimized.

Show comment
Hide comment
@tikue

tikue Aug 29, 2016

Contributor

Is it planned for ! to affect pattern matching exhaustiveness? Example of current, possibly-wrong behavior:

#![feature(never_type)]

fn main() {
    let result: Result<_, !> = Ok(1);
    match result {
//        ^^^^^^ pattern `Err(_)` not covered
        Ok(i) => println!("{}", i),
    }
}
Contributor

tikue commented Aug 29, 2016

Is it planned for ! to affect pattern matching exhaustiveness? Example of current, possibly-wrong behavior:

#![feature(never_type)]

fn main() {
    let result: Result<_, !> = Ok(1);
    match result {
//        ^^^^^^ pattern `Err(_)` not covered
        Ok(i) => println!("{}", i),
    }
}
@lfairy

This comment has been minimized.

Show comment
Hide comment
@lfairy

lfairy Aug 29, 2016

Contributor

@tikue yes, it's one of the bugs listed above.

Contributor

lfairy commented Aug 29, 2016

@tikue yes, it's one of the bugs listed above.

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Jul 9, 2018

Contributor

That's it, if during coercion we encounter an obligation of the form $0: Unsize<Y> where Y is definitely unsized and $0 is definitely sized, don't treat that as an ambiguity but rather treat it as an "ok" and proceed with the unsized coercion.

Contributor

arielb1 commented Jul 9, 2018

That's it, if during coercion we encounter an obligation of the form $0: Unsize<Y> where Y is definitely unsized and $0 is definitely sized, don't treat that as an ambiguity but rather treat it as an "ok" and proceed with the unsized coercion.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 10, 2018

@arielb1

So how this is different than coercion a Box::new(()) to Box<Debug>? std::error::Error is a trait like Debug, not a type like [u8]. std::io::Error on the other hand is a type, but it is Sized.

We never have trouble of the following:

use std::fmt::Debug;

fn f(x:()) -> Box<Debug> {
    Box::new(x)
}

fn main() {
}

earthengine commented Jul 10, 2018

@arielb1

So how this is different than coercion a Box::new(()) to Box<Debug>? std::error::Error is a trait like Debug, not a type like [u8]. std::io::Error on the other hand is a type, but it is Sized.

We never have trouble of the following:

use std::fmt::Debug;

fn f(x:()) -> Box<Debug> {
    Box::new(x)
}

fn main() {
}
@bjorn3

This comment has been minimized.

Show comment
Hide comment
@bjorn3

bjorn3 Jul 10, 2018

Contributor

@earthengine the difference between this issue and your code is:

Written Box::new(!): Box<Debug> Box::new(()): Box<Debug>
Performed Box::new(! as Debug): Debug (Box::new(()) as Box<Debug>): Debug
Contributor

bjorn3 commented Jul 10, 2018

@earthengine the difference between this issue and your code is:

Written Box::new(!): Box<Debug> Box::new(()): Box<Debug>
Performed Box::new(! as Debug): Debug (Box::new(()) as Box<Debug>): Debug
@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Jul 10, 2018

Member

Essentially, because ! can coerce to anything, it is coerced to Debug before the call to Box::new. That's not an option with (), so there is no problem in the second case -- the coercion there happens after Box::new.

Member

RalfJung commented Jul 10, 2018

Essentially, because ! can coerce to anything, it is coerced to Debug before the call to Box::new. That's not an option with (), so there is no problem in the second case -- the coercion there happens after Box::new.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 10, 2018

@RalfJung

My brain model is that DSTs cannot exist in the wild - they have to come from some more concret types. This is even true when we allow DSTs in the parameter position - if not in the return position. That said, until the real value being placed already behind a pointer, it should not be able to coace to a DST, even it is !.

For example the following should not compile:

let v: str = !;
let v: [u8] = !;
let v: dyn Debug = !;

Simply because you cannot replace ! with any existing Rust expression to make it compile.

Edit

That's not an option with ()

So can anyone explain why? If () as dyn Debug does not compile, ! as dyn Debug shall not and vice versa. &() as &Debug compiles, so do &! as &Debug, no problems. If () as dyn Debug may some day compiles, the problem we have today will be repeated for (), so the DST RFC implementors will have to due with it, and so it will solve the same problem we have for !.

earthengine commented Jul 10, 2018

@RalfJung

My brain model is that DSTs cannot exist in the wild - they have to come from some more concret types. This is even true when we allow DSTs in the parameter position - if not in the return position. That said, until the real value being placed already behind a pointer, it should not be able to coace to a DST, even it is !.

For example the following should not compile:

let v: str = !;
let v: [u8] = !;
let v: dyn Debug = !;

Simply because you cannot replace ! with any existing Rust expression to make it compile.

Edit

That's not an option with ()

So can anyone explain why? If () as dyn Debug does not compile, ! as dyn Debug shall not and vice versa. &() as &Debug compiles, so do &! as &Debug, no problems. If () as dyn Debug may some day compiles, the problem we have today will be repeated for (), so the DST RFC implementors will have to due with it, and so it will solve the same problem we have for !.

@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Jul 10, 2018

Member

! coerces to anything because it is impossible to exist. "Ex falso quodlibet". So all of your examples should compile -- there is no good theoretical reason to make a special exception for unsized types.

This does not even violate "DTS cannot exist" because ! cannot exist, either. :)

() as dyn Debug does not compile

I don't know what the plans are for unsized rvalues, but I guess they could make this compile?
The point is, doing this for ! does not require us to implement unsized rvalues because this can only happen in dead code.

However, maybe you are pointing at a possible solution here (and maybe that's what @arielb1 also suggested above): Is it possible to restrict the ! coercion to only apply if the target type is (known to be) sized? There's no good theoretical reason to do this but a practical one. :D Namely, that it might help to solve this problem.

Member

RalfJung commented Jul 10, 2018

! coerces to anything because it is impossible to exist. "Ex falso quodlibet". So all of your examples should compile -- there is no good theoretical reason to make a special exception for unsized types.

This does not even violate "DTS cannot exist" because ! cannot exist, either. :)

() as dyn Debug does not compile

I don't know what the plans are for unsized rvalues, but I guess they could make this compile?
The point is, doing this for ! does not require us to implement unsized rvalues because this can only happen in dead code.

However, maybe you are pointing at a possible solution here (and maybe that's what @arielb1 also suggested above): Is it possible to restrict the ! coercion to only apply if the target type is (known to be) sized? There's no good theoretical reason to do this but a practical one. :D Namely, that it might help to solve this problem.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 10, 2018

When I said "DTS cannot exist" I mean in syntactical. It is not possible to have a local variable that is typed is str, for example.

On the other hand, ! cannot exist is semantical. You can write

let v = exit(0);

to have v syntactically typed ! in the context, but of cause the binding will not even run and so it does not exit in the real world.

So here is the reasonale: we allow ! coerce to any type, only when you can write an expression that have the same type. If a type cannot even exist syntactically, it should not be allowed.

This also apply to unsized rvalues, so before we have unsized rvalues, coerce ! to unsized types is not allowed, until we know how to handle unsized rvalues. And only at that point we can start think about this problem (one obvious solution seems to be allow Box::new receive unsized values).

earthengine commented Jul 10, 2018

When I said "DTS cannot exist" I mean in syntactical. It is not possible to have a local variable that is typed is str, for example.

On the other hand, ! cannot exist is semantical. You can write

let v = exit(0);

to have v syntactically typed ! in the context, but of cause the binding will not even run and so it does not exit in the real world.

So here is the reasonale: we allow ! coerce to any type, only when you can write an expression that have the same type. If a type cannot even exist syntactically, it should not be allowed.

This also apply to unsized rvalues, so before we have unsized rvalues, coerce ! to unsized types is not allowed, until we know how to handle unsized rvalues. And only at that point we can start think about this problem (one obvious solution seems to be allow Box::new receive unsized values).

@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Jul 10, 2018

Member

It is not possible to have a local variable that is typed is str, for example.

That's just a limitation of the current implementation: #48055

Member

RalfJung commented Jul 10, 2018

It is not possible to have a local variable that is typed is str, for example.

That's just a limitation of the current implementation: #48055

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Jul 10, 2018

Contributor

@RalfJung

That's not the problem here.

Again, suppose we're at the situation where Box::new(!: $0): Box<dyn fmt::Debug> where $0 is a type variable.

Then the compiler can fairly easily deduce that $0: Sized (because $0 is equal to the type parameter of Box::new, which has a T: Sized bound).

The problem comes where the compiler needs to figure out which sort of coercion to use to transform the Box<$0> to a Box<dyn fmt::Debug>. From a "local" POV, there are 2 solutions:

  1. Have a CoerceUnsized coercion, requiring Box<$0>: CoerceUnsized<Box<dyn fmt::Debug>>. This is a valid program for $0 = ! and defaulting will make it compile to that.
  2. Have an identity coercion, with $0 = dyn fmt::Debug. This is inconsistent with the $0: Sized requirement.

The compiler doesn't want to have too much stuff open when considering ambiguities, as that can cause both performance issues and hard-to-debug problems, so it picks out which coercion to use in a fairly stupid manner (in particular, we don't have T: CoerceUnsized<T>, so if the compiler picks option 1 it can get "stuck" pretty easily), and it ends up picking option 2, which fails. I had an idea to make it a little smarter and pick option 1.

Contributor

arielb1 commented Jul 10, 2018

@RalfJung

That's not the problem here.

Again, suppose we're at the situation where Box::new(!: $0): Box<dyn fmt::Debug> where $0 is a type variable.

Then the compiler can fairly easily deduce that $0: Sized (because $0 is equal to the type parameter of Box::new, which has a T: Sized bound).

The problem comes where the compiler needs to figure out which sort of coercion to use to transform the Box<$0> to a Box<dyn fmt::Debug>. From a "local" POV, there are 2 solutions:

  1. Have a CoerceUnsized coercion, requiring Box<$0>: CoerceUnsized<Box<dyn fmt::Debug>>. This is a valid program for $0 = ! and defaulting will make it compile to that.
  2. Have an identity coercion, with $0 = dyn fmt::Debug. This is inconsistent with the $0: Sized requirement.

The compiler doesn't want to have too much stuff open when considering ambiguities, as that can cause both performance issues and hard-to-debug problems, so it picks out which coercion to use in a fairly stupid manner (in particular, we don't have T: CoerceUnsized<T>, so if the compiler picks option 1 it can get "stuck" pretty easily), and it ends up picking option 2, which fails. I had an idea to make it a little smarter and pick option 1.

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Jul 10, 2018

Contributor

So thinking on it from this POV, the right idea might be to trigger an unsized coercion if

  1. The unsized coercion is self-consistent (that would be the current rules, except that ambiguity is OK).
  2. not triggering the unsized coercion is inconsistent. We have to see that this can be calculated without ruining performance on larger functions.
Contributor

arielb1 commented Jul 10, 2018

So thinking on it from this POV, the right idea might be to trigger an unsized coercion if

  1. The unsized coercion is self-consistent (that would be the current rules, except that ambiguity is OK).
  2. not triggering the unsized coercion is inconsistent. We have to see that this can be calculated without ruining performance on larger functions.
@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 10, 2018

Have an identity coercion, with $0 = dyn fmt::Debug. This is inconsistent with the $0: Sized requirement.

I can hardly see that why should us allow let v: str=! but not let v: str;. If you cannot even declare a variable with a specific type, why can you bind a (maybe impossible) value to it?

When unsized rvalue is not shipped, the consistent way is not to allow any coercion to unsized types, as this will create (maybe temporary) unsized rvalues, even when this happens only in imagination.

So my conclusion is that the Box::new(!) as Box<Error> problem is a blocking issue for unsized rvalues, not for the ! type. The ! coercion rules should be:

You can write let v: T, if and only if you can write let v: T = !.

Thea actual meaning will then evaluate with the language. Especially, after we have unsized rvalues, we have Box::new(! as dyn Debug) but before that, I would say no.

So what we can do to move forward?

Add a feature gate on the code that triggers the unsized coercion, if unsized rvalue is on, try this coercion, otherwise, skip it. If this is the only available coercion, the error message should be "the trait bound str: std::marker::Sized is not satisfied", like in similar cases. So

We have to see that this can be calculated without ruining performance on larger functions.

is addressed: just a simple feature check.

Meanwhile, add a new issue for unsized rvalues, and have the link added to the tracking issue, so to make sure this issue is addressed properly.

earthengine commented Jul 10, 2018

Have an identity coercion, with $0 = dyn fmt::Debug. This is inconsistent with the $0: Sized requirement.

I can hardly see that why should us allow let v: str=! but not let v: str;. If you cannot even declare a variable with a specific type, why can you bind a (maybe impossible) value to it?

When unsized rvalue is not shipped, the consistent way is not to allow any coercion to unsized types, as this will create (maybe temporary) unsized rvalues, even when this happens only in imagination.

So my conclusion is that the Box::new(!) as Box<Error> problem is a blocking issue for unsized rvalues, not for the ! type. The ! coercion rules should be:

You can write let v: T, if and only if you can write let v: T = !.

Thea actual meaning will then evaluate with the language. Especially, after we have unsized rvalues, we have Box::new(! as dyn Debug) but before that, I would say no.

So what we can do to move forward?

Add a feature gate on the code that triggers the unsized coercion, if unsized rvalue is on, try this coercion, otherwise, skip it. If this is the only available coercion, the error message should be "the trait bound str: std::marker::Sized is not satisfied", like in similar cases. So

We have to see that this can be calculated without ruining performance on larger functions.

is addressed: just a simple feature check.

Meanwhile, add a new issue for unsized rvalues, and have the link added to the tracking issue, so to make sure this issue is addressed properly.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 11, 2018

Insteresting.

This

fn main() {
    let _:str = *"";
}

compiles, but

fn main() {
    let v:str = *"";
}

do not. This is not even related to the ! type. Shall I create an issue for this?

earthengine commented Jul 11, 2018

Insteresting.

This

fn main() {
    let _:str = *"";
}

compiles, but

fn main() {
    let v:str = *"";
}

do not. This is not even related to the ! type. Shall I create an issue for this?

@SimonSapin

This comment has been minimized.

Show comment
Hide comment
@SimonSapin

SimonSapin Jul 11, 2018

Contributor

I don’t know if that last one really is a bug. The second example does not compile because, without unsized rvalue support, the compiler wants to know statically how much stack space to allocate for the local variable v but can’t because it’s a DST.

In the first example, the _ pattern is special in that it not only matches anything (like a local variable binding), but it does not even create a variable at all. So that code is the same as fn main() { *""; } without let. Dereferencing a reference (even DST) and then doing nothing with the result is not useful, but it seems to be valid and I’m not convinced it should be invalid.

Contributor

SimonSapin commented Jul 11, 2018

I don’t know if that last one really is a bug. The second example does not compile because, without unsized rvalue support, the compiler wants to know statically how much stack space to allocate for the local variable v but can’t because it’s a DST.

In the first example, the _ pattern is special in that it not only matches anything (like a local variable binding), but it does not even create a variable at all. So that code is the same as fn main() { *""; } without let. Dereferencing a reference (even DST) and then doing nothing with the result is not useful, but it seems to be valid and I’m not convinced it should be invalid.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 11, 2018

Right. But it is really confusing, especially the following

fn main() {
    let _v:str = *"";
}

does not compile either. With your theory about _ this should be the same except that we call the unused thing _v rather than just _.

earthengine commented Jul 11, 2018

Right. But it is really confusing, especially the following

fn main() {
    let _v:str = *"";
}

does not compile either. With your theory about _ this should be the same except that we call the unused thing _v rather than just _.

@ssokolow

This comment has been minimized.

Show comment
Hide comment
@ssokolow

ssokolow Jul 11, 2018

From what I remember, the only difference between _v and v is that the leading underscore suppresses warnings about unused values.

On the other hand, _ specifically means "discard it" and is handled specially in order to enable it to appear in more than one place in a pattern (eg. tuple unpack, arguments list, etc.) without causing an error about a name collision.

ssokolow commented Jul 11, 2018

From what I remember, the only difference between _v and v is that the leading underscore suppresses warnings about unused values.

On the other hand, _ specifically means "discard it" and is handled specially in order to enable it to appear in more than one place in a pattern (eg. tuple unpack, arguments list, etc.) without causing an error about a name collision.

@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Jul 11, 2018

Member

On the other hand, _ specifically means "discard it" and is handled specially in order to enable it to appear in more than one place in a pattern (eg. tuple unpack, arguments list, etc.) without causing an error about a name collision.

Correct. On top of what you said, let _ = foo() leads to drop being called immediately, whereas _v only gets dropped when it goes out of scope (like v would).

Member

RalfJung commented Jul 11, 2018

On the other hand, _ specifically means "discard it" and is handled specially in order to enable it to appear in more than one place in a pattern (eg. tuple unpack, arguments list, etc.) without causing an error about a name collision.

Correct. On top of what you said, let _ = foo() leads to drop being called immediately, whereas _v only gets dropped when it goes out of scope (like v would).

@SimonSapin

This comment has been minimized.

Show comment
Hide comment
@SimonSapin

SimonSapin Jul 11, 2018

Contributor

Indeed, this is what I meant by “_ is special”.

Contributor

SimonSapin commented Jul 11, 2018

Indeed, this is what I meant by “_ is special”.

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 11, 2018

So now it looks like denying all unsized rvalues (unless feature "unsized rvalues" turned on) in the wild would be a breaking change, although the effect would be little I think.

I then still suggest to have a feature gate to disallow coersion from ! to unsized rvalues. This would deny code like let _:str = return;, but I don't think any one will use it in code.

earthengine commented Jul 11, 2018

So now it looks like denying all unsized rvalues (unless feature "unsized rvalues" turned on) in the wild would be a breaking change, although the effect would be little I think.

I then still suggest to have a feature gate to disallow coersion from ! to unsized rvalues. This would deny code like let _:str = return;, but I don't think any one will use it in code.

@SimonSapin

This comment has been minimized.

Show comment
Hide comment
@SimonSapin

SimonSapin Jul 11, 2018

Contributor

As long as the ! type is unstable we can make breaking changes to where it can or cannot be coerced.

The question is, if we stabilize it without coercion to DSTs in order to fix #49593, will we want to restore such coercion later when we add unsized rvalues and would we be able to do so without breaking #49593 again?

Contributor

SimonSapin commented Jul 11, 2018

As long as the ! type is unstable we can make breaking changes to where it can or cannot be coerced.

The question is, if we stabilize it without coercion to DSTs in order to fix #49593, will we want to restore such coercion later when we add unsized rvalues and would we be able to do so without breaking #49593 again?

@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 11, 2018

My previous proposal includes this. #49593 should be a blocking issue for unsized rvalues.

My personal suggestion for the final solution, is to let Box::new (may also include some other important methods) accepts unsized parameters, and allow ambiguities in similar situations.

earthengine commented Jul 11, 2018

My previous proposal includes this. #49593 should be a blocking issue for unsized rvalues.

My personal suggestion for the final solution, is to let Box::new (may also include some other important methods) accepts unsized parameters, and allow ambiguities in similar situations.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Jul 13, 2018

Member

FWIW, _ is nothing like a special variable name. It's an entirely separate pattern syntax which does nothing to the place being matched. Pattern-matching works on places, not values.
The closest it comes to a variable binding is ref _x, but even then you probably need NLL to avoid conflicting borrowing resulting from it. This works btw:

// The type `str` is the type of the place being matched. `x` has type `&str`.
let ref x: str = *"foo";
// Fully equivalent to this:
let x: &str = &*"foo";
Member

eddyb commented Jul 13, 2018

FWIW, _ is nothing like a special variable name. It's an entirely separate pattern syntax which does nothing to the place being matched. Pattern-matching works on places, not values.
The closest it comes to a variable binding is ref _x, but even then you probably need NLL to avoid conflicting borrowing resulting from it. This works btw:

// The type `str` is the type of the place being matched. `x` has type `&str`.
let ref x: str = *"foo";
// Fully equivalent to this:
let x: &str = &*"foo";
@earthengine

This comment has been minimized.

Show comment
Hide comment
@earthengine

earthengine Jul 13, 2018

@eddyb

You didn't get my point. My assumsion is that in Rust there is currently no unsized rvalues can appear in code at all. However it turns out that they appear in let _:str = *"". Although such a value can live for no time, in syntatical level it does live. Something like a ghost...

Your examples instead, is technically fully legal and it is not the point. str is unsized, but &str is sized.

earthengine commented Jul 13, 2018

@eddyb

You didn't get my point. My assumsion is that in Rust there is currently no unsized rvalues can appear in code at all. However it turns out that they appear in let _:str = *"". Although such a value can live for no time, in syntatical level it does live. Something like a ghost...

Your examples instead, is technically fully legal and it is not the point. str is unsized, but &str is sized.

@SimonSapin

This comment has been minimized.

Show comment
Hide comment
@SimonSapin

SimonSapin Jul 13, 2018

Contributor

However it turns out that they appear in let _:str = *"". Although such a value can live for no time, in syntatical level it does live. Something like a ghost...

There is a similar "ghost" in an expression like &*foo. First you dereference foo and then take the address of the result without moving it. So it’s not the same as & { *foo }, for example.

Contributor

SimonSapin commented Jul 13, 2018

However it turns out that they appear in let _:str = *"". Although such a value can live for no time, in syntatical level it does live. Something like a ghost...

There is a similar "ghost" in an expression like &*foo. First you dereference foo and then take the address of the result without moving it. So it’s not the same as & { *foo }, for example.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Jul 13, 2018

Member

@earthengine @SimonSapin No unsized rvalues ("value expression") exist there. Only lvalues ("place expressions") do. *"foo" is a place, and pattern-matching it doesn't require a value, only a place.

The names "lvalue" and "rvalue" are relics and somewhat misleading in C too, but even worse in Rust, because let's RHS is an "lvalue" (place expression), despite the name.
(Assignment expressions are the only place where the C names and rules sort of make sense in Rust)

In your examples, let x = *"foo";, the value is required to bind it to x.
Similarly, &*"foo" creates a place expression then borrows it, without ever needing to create a an unsized value, while {*"foo"} is always a value expression and thus doesn't allow unsized types.

Member

eddyb commented Jul 13, 2018

@earthengine @SimonSapin No unsized rvalues ("value expression") exist there. Only lvalues ("place expressions") do. *"foo" is a place, and pattern-matching it doesn't require a value, only a place.

The names "lvalue" and "rvalue" are relics and somewhat misleading in C too, but even worse in Rust, because let's RHS is an "lvalue" (place expression), despite the name.
(Assignment expressions are the only place where the C names and rules sort of make sense in Rust)

In your examples, let x = *"foo";, the value is required to bind it to x.
Similarly, &*"foo" creates a place expression then borrows it, without ever needing to create a an unsized value, while {*"foo"} is always a value expression and thus doesn't allow unsized types.

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Jul 16, 2018

Contributor

In #52420 I hit that

let Ok(b): Result<B, !> = ...;
b

no longer works. Is that intentional?

Contributor

Ericson2314 commented Jul 16, 2018

In #52420 I hit that

let Ok(b): Result<B, !> = ...;
b

no longer works. Is that intentional?

@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Jul 16, 2018

Member

AFAIK yes -- the infallible pattern story is complicated and has a separate feature gate from ! itself. Also see rust-lang/rfcs#1872 and #48950.

Member

RalfJung commented Jul 16, 2018

AFAIK yes -- the infallible pattern story is complicated and has a separate feature gate from ! itself. Also see rust-lang/rfcs#1872 and #48950.

@Nemo157

This comment has been minimized.

Show comment
Hide comment
@Nemo157

Nemo157 Jul 16, 2018

Contributor

@Ericson2314 specifically the feature you would need to add to liballoc is exhaustive_patterns.

Contributor

Nemo157 commented Jul 16, 2018

@Ericson2314 specifically the feature you would need to add to liballoc is exhaustive_patterns.

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Jul 16, 2018

Contributor

Thanks!!

Contributor

Ericson2314 commented Jul 16, 2018

Thanks!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment