Skip to content
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

Allow coercions from never-type when ref binding is involved #118270

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Aaron1011
Copy link
Member

@Aaron1011 Aaron1011 commented Nov 24, 2023

Fixes #118113

When we type-check a binding that uses ref patterns, we do not perform coercions between the expression type and the pattern type.

However, this has the unfortunate result of disallow code like: let Foo { ref my_field } = diverging_expr();. This code is accepted without the ref keyword.

We now explicitly allow coercions when the expression type is ! In all other cases, we still avoid performing coersions. This avoids causing broader changes in type-checking (with potential soundness implications for 'ref mut'),

@rustbot
Copy link
Collaborator

rustbot commented Nov 24, 2023

r? @compiler-errors

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Nov 24, 2023
Copy link
Member

@compiler-errors compiler-errors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only got one concern about changing the check_expr_with_needs.


Can you add a test where the source type is an associated type that normalizes to !?

trait Call {
  type Out;
  fn call(self) -> Self::Out;
}

impl<F: FnOnce() -> T, T> Call for F {
  type Out = T;
  fn call(self) -> T { (self)() }
}

pub struct Foo {
    bar: u8
}

fn diverge() -> ! { todo!() }

#[allow(unused_variables)]
fn main() {
    let Foo { ref bar } = diverge.call();
}

Just so we don't make sure to subtly regress this when lazy normalization comes around.

let init_ty = self.check_expr_with_needs(init, Needs::maybe_mut_place(m));
let mut init_ty = self.check_expr_with_expectation_and_needs(
init,
ExpectHasType(local_ty),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I wonder if this has subtle inference implications. Did you look into this?

Also, why did you add this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the call made by check_expr_coercible_to_type (which we call in the non-bindings case) - I wanted to align them more closely.

When we type-check a binding that uses ref patterns, we do not
perform coercions between the expression type and the pattern type.

However, this has the unfortunate result of disallow code like:
`let Foo { ref my_field } = diverging_expr();`. This code is accepted
without the `ref` keyword.

We now explicitly allow coercions when the expression type is !
In all other cases, we still avoid performing coersions.
This avoids causing broader changes in type-checking (with potential
soundness implications for 'ref mut'),
@Aaron1011
Copy link
Member Author

@compiler-errors I've added an additional test

@compiler-errors compiler-errors added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Dec 14, 2023
@compiler-errors
Copy link
Member

Sorry for taking so long to get to this. I think this behavior is fine, but I don't feel comfortable approving this without some consensus. I'll start a types FCP.

@rfcbot fcp merge

See description and test for what this enables. Seems pretty clear to me.

@rfcbot
Copy link

rfcbot commented Dec 14, 2023

Team member @compiler-errors has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Dec 14, 2023
@compiler-errors compiler-errors added S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 14, 2023
// to be unreachable, so the soundness concerns with 'ref mut' do not apply.
if init_ty.is_never() {
init_ty = self.demand_coerce(init, init_ty, local_ty, None, AllowTwoPhase::No);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
};
}

@BoxyUwU
Copy link
Member

BoxyUwU commented Jan 15, 2024

this should possibly be a lang question too as to whether we would want the behaviour to be a check for Ty::Never instead of the type being uninhabited

@lcnr
Copy link
Contributor

lcnr commented Jan 15, 2024

@rfcbot concern want to look into always enabling coercions

even if there is a ref we should still detect any unsoundness in thir unsafeck or borrowck, so I don't think the original concerns still apply. Want to look into this. Specialcasing only ! here feels less than ideal

@Nadrieril
Copy link
Contributor

We don't coerce uninhabited types in general today, only !; we should keep that here

@matthewjasper
Copy link
Contributor

This seems the exact opposite decision to the one made in #117288 (comment)

@Nadrieril
Copy link
Contributor

Nadrieril commented Feb 5, 2024

In my understanding, this is consistent: #117288 is about whether constructing a place of type ! should coerce, and the decision was "no". This applies because a _ pattern doesn't do anything to the place. This current issue however involves an explicit pattern, which counts as accessing the place. It's the access that triggers the NeverToAny. This is comparable to this case mentioned in #117288 (comment), where NeverToAny does happen:

let _val: () = *x; //~ Insta-UB.

@lcnr
Copy link
Contributor

lcnr commented Feb 6, 2024

@rfcbot resolve want to look into always enabling coercions

while I still lack quite a lot of context here, I will sadly not get to actually spend significantly more time on this :/ relying on

In my understanding, this is consistent: #117288 is about whether constructing a place of type ! should coerce, and the decision was "no". This applies because a _ pattern doesn't do anything to the place. This current issue however involves an explicit pattern, which counts as accessing the place.

cc @RalfJung @digama0 who seem to have a lot more context here given their comments in #117288.

I think that assuming we actually convert the place to a value in the following example, I feel fairly confident that it's a step in the right direction, regardless of whether we want to also allow more general coercions if there are ref bindings in the future:

struct Foo(u8);
// This currently does not coerce but afaict will with this PR.
// Please add this as a test.
fn should_coerce_with_this_pr() {
    let ref foo: Foo = loop {};
}

// this currently coerces and afaict it should ideally
// not do so according to RalfJ in #117288.
fn already_coerces() {
    let _: Foo = loop {};
}

Can I get some confirmation whether let ref foo: Foo = expr; converts expr to a value?1 Note that due to the ref we refer to the place from expr:

use std::cell::Cell;

fn ref_pat(x: &Cell<bool>) {
    let ref new_val_same_place = *x;
    new_val_same_place.set(true);
}

fn main() {
    let x = Cell::new(false);
    ref_pat(&x);
    assert!(x.get());
}

Footnotes

  1. afaict equivalent question: should let ref x: ! = *ptr_to_never be UB?

@RalfJung
Copy link
Member

RalfJung commented Feb 6, 2024

It's not at all clear to me that let ref x = expr should be constructing a value -- AFAIK it is equivalent to let x = &expr, which evaluates expr as a place only.

For let Foo { ref my_field } = expr;, that seems equivalent to let my_field = &expr.my_field. expr is in place position here so again, I don't think this constructs a value.

@RalfJung
Copy link
Member

RalfJung commented Feb 6, 2024

The example in #118113 is a bit different, since expr there is a function call expression, which is a value expression, so indeed if it has type ! then it can be coerced to anything.

But I don't think we want to accept code like this:

let raw_ptr = 16 as *const !;
let Foo { ref my_field } = *raw_ptr;

This is taking a place of type ! and trying to use it as a Foo; that's just nonsense that we should reject.

@lcnr
Copy link
Contributor

lcnr commented Feb 6, 2024

@RalfJung to make sure I understand you correctly:

First a question: is there currently a consensus whether a place expression of type ! is unreachable? Afaict the consensus is that it is not. A place of type ! is totally acceptable and only causes UB when converted to a value. let _ = *(&1 as *const u32 as *const !); is not UB.

It's not at all clear to me that let ref x = expr should be constructing a value -- AFAIK it is equivalent to let x = &expr, which evaluates expr as a place only.

For let Foo { ref my_field } = expr;, that seems equivalent to let my_field = &expr.my_field. expr is in place position here so again, I don't think this constructs a value.

You believe that let ref var = place_expr does not convert place_expr to a value and neither does let Foo { ref my_field } = *raw_ptr;.

You therefore believe that the PR as is is not desirable. We should only apply coercions from ! to any other type if the source is an expression, not a place?

Assuming that 1) we have not yet decided that a place of type ! is UB (or even decided the opposite) and 2) this PR would result in coercions from places to pattern to be unique for ! by adding a place_to_value conversion not present for other types:

@rfcbot concern coerce place vs value

I am not confident that I fully understand what is going on here and how this fits into the greater picture, but I would like to definitely involve @rust-lang/opsem to make sure the behavior here is consistent

@digama0
Copy link
Contributor

digama0 commented Feb 7, 2024

I believe @RalfJung and I were in agreement on #117288 that constructing places of type ! should not be UB, and the lang team conclusion at the end is also in accordance with this, but AFAIK there has been no FCP about it. (The disagreement was about whether coercions can be applied to never places, which necessarily require place-to-value conversion and therefore cause UB.) The lang team conclusion seems to be in favor of removing coercions on never places completely, meaning you need { *x } to get a value before coercions trigger. This is not consistent with current stable behavior, so this may need to be done over an edition boundary.

As it relates to this PR, I think it's okay provided that it only triggers on expressions that are already value expressions (like diverging_expr()). Triggering on *x where x: *const ! would be exactly the opposite of the conclusion in #117288.

@RalfJung
Copy link
Member

A place of type ! is totally acceptable and only causes UB when converted to a value. let _ = *(&1 as *const u32 as *const !); is not UB.

I think that is the general opinion in @rust-lang/opsem, yes.

You therefore believe that the PR as is is not desirable. We should only apply coercions from ! to any other type if the source is an expression, not a place?

If the source is a value expression. Yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Using let pattern with ref results in 'mismatched types' when combined with 'panic!()'