-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Do not consider match/let/ref of place that evaluates to !
to diverge, disallow coercions from them too
#129392
Changes from all commits
6371ef6
5193c21
73d49f8
515b932
d2bd018
e8d5eb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
// ignore-tidy-filelength | ||
// FIXME: we should move the field error reporting code somewhere else. | ||
|
||
//! Type checking expressions. | ||
//! | ||
//! See [`rustc_hir_analysis::check`] for more context on type checking in general. | ||
|
@@ -62,7 +65,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { | |
|
||
// While we don't allow *arbitrary* coercions here, we *do* allow | ||
// coercions from ! to `expected`. | ||
if ty.is_never() { | ||
if ty.is_never() && self.expr_guaranteed_to_constitute_read_for_never(expr) { | ||
if let Some(_) = self.typeck_results.borrow().adjustments().get(expr.hir_id) { | ||
let reported = self.dcx().span_delayed_bug( | ||
expr.span, | ||
|
@@ -238,8 +241,11 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { | |
_ => self.warn_if_unreachable(expr.hir_id, expr.span, "expression"), | ||
} | ||
|
||
// Any expression that produces a value of type `!` must have diverged | ||
if ty.is_never() { | ||
// Any expression that produces a value of type `!` must have diverged, | ||
// unless it's a place expression that isn't being read from, in which case | ||
// diverging would be unsound since we may never actually read the `!`. | ||
// e.g. `let _ = *never_ptr;` with `never_ptr: *const !`. | ||
if ty.is_never() && self.expr_guaranteed_to_constitute_read_for_never(expr) { | ||
self.diverges.set(self.diverges.get() | Diverges::always(expr.span)); | ||
} | ||
|
||
|
@@ -257,6 +263,185 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> { | |
ty | ||
} | ||
|
||
/// Whether this expression constitutes a read of value of the type that | ||
/// it evaluates to. | ||
/// | ||
/// This is used to determine if we should consider the block to diverge | ||
/// if the expression evaluates to `!`, and if we should insert a `NeverToAny` | ||
/// coercion for values of type `!`. | ||
/// | ||
/// This function generally returns `false` if the expression is a place | ||
/// expression and the *parent* expression is the scrutinee of a match or | ||
/// the pointee of an `&` addr-of expression, since both of those parent | ||
/// expressions take a *place* and not a value. | ||
pub(super) fn expr_guaranteed_to_constitute_read_for_never( | ||
&self, | ||
expr: &'tcx hir::Expr<'tcx>, | ||
) -> bool { | ||
// We only care about place exprs. Anything else returns an immediate | ||
// which would constitute a read. We don't care about distinguishing | ||
// "syntactic" place exprs since if the base of a field projection is | ||
// not a place then it would've been UB to read from it anyways since | ||
// that constitutes a read. | ||
if !expr.is_syntactic_place_expr() { | ||
return true; | ||
} | ||
|
||
let parent_node = self.tcx.parent_hir_node(expr.hir_id); | ||
match parent_node { | ||
hir::Node::Expr(parent_expr) => { | ||
match parent_expr.kind { | ||
// Addr-of, field projections, and LHS of assignment don't constitute reads. | ||
// Assignment does call `drop_in_place`, though, but its safety | ||
// requirements are not the same. | ||
ExprKind::AddrOf(..) | hir::ExprKind::Field(..) => false, | ||
ExprKind::Assign(lhs, _, _) => { | ||
// Only the LHS does not constitute a read | ||
expr.hir_id != lhs.hir_id | ||
} | ||
|
||
// See note on `PatKind::Or` below for why this is `all`. | ||
ExprKind::Match(scrutinee, arms, _) => { | ||
assert_eq!(scrutinee.hir_id, expr.hir_id); | ||
arms.iter() | ||
.all(|arm| self.pat_guaranteed_to_constitute_read_for_never(arm.pat)) | ||
} | ||
ExprKind::Let(hir::LetExpr { init, pat, .. }) => { | ||
assert_eq!(init.hir_id, expr.hir_id); | ||
self.pat_guaranteed_to_constitute_read_for_never(*pat) | ||
} | ||
|
||
// Any expression child of these expressions constitute reads. | ||
ExprKind::Array(_) | ||
| ExprKind::Call(_, _) | ||
| ExprKind::MethodCall(_, _, _, _) | ||
| ExprKind::Tup(_) | ||
| ExprKind::Binary(_, _, _) | ||
| ExprKind::Unary(_, _) | ||
lcnr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ExprKind::Cast(_, _) | ||
| ExprKind::Type(_, _) | ||
| ExprKind::DropTemps(_) | ||
| ExprKind::If(_, _, _) | ||
| ExprKind::Closure(_) | ||
| ExprKind::Block(_, _) | ||
| ExprKind::AssignOp(_, _, _) | ||
| ExprKind::Index(_, _, _) | ||
| ExprKind::Break(_, _) | ||
| ExprKind::Ret(_) | ||
| ExprKind::Become(_) | ||
| ExprKind::InlineAsm(_) | ||
| ExprKind::Struct(_, _, _) | ||
| ExprKind::Repeat(_, _) | ||
| ExprKind::Yield(_, _) => true, | ||
|
||
// These expressions have no (direct) sub-exprs. | ||
ExprKind::ConstBlock(_) | ||
| ExprKind::Loop(_, _, _, _) | ||
| ExprKind::Lit(_) | ||
| ExprKind::Path(_) | ||
| ExprKind::Continue(_) | ||
| ExprKind::OffsetOf(_, _) | ||
| ExprKind::Err(_) => unreachable!("no sub-expr expected for {:?}", expr.kind), | ||
} | ||
} | ||
|
||
// If we have a subpattern that performs a read, we want to consider this | ||
// to diverge for compatibility to support something like `let x: () = *never_ptr;`. | ||
hir::Node::LetStmt(hir::LetStmt { init: Some(target), pat, .. }) => { | ||
assert_eq!(target.hir_id, expr.hir_id); | ||
self.pat_guaranteed_to_constitute_read_for_never(*pat) | ||
} | ||
|
||
// These nodes (if they have a sub-expr) do constitute a read. | ||
hir::Node::Block(_) | ||
| hir::Node::Arm(_) | ||
| hir::Node::ExprField(_) | ||
| hir::Node::AnonConst(_) | ||
| hir::Node::ConstBlock(_) | ||
| hir::Node::ConstArg(_) | ||
| hir::Node::Stmt(_) | ||
| hir::Node::Item(hir::Item { | ||
kind: hir::ItemKind::Const(..) | hir::ItemKind::Static(..), | ||
.. | ||
}) | ||
| hir::Node::TraitItem(hir::TraitItem { | ||
kind: hir::TraitItemKind::Const(..), .. | ||
}) | ||
| hir::Node::ImplItem(hir::ImplItem { kind: hir::ImplItemKind::Const(..), .. }) => true, | ||
|
||
// These nodes do not have direct sub-exprs. | ||
hir::Node::Param(_) | ||
| hir::Node::Item(_) | ||
| hir::Node::ForeignItem(_) | ||
| hir::Node::TraitItem(_) | ||
| hir::Node::ImplItem(_) | ||
| hir::Node::Variant(_) | ||
| hir::Node::Field(_) | ||
| hir::Node::PathSegment(_) | ||
| hir::Node::Ty(_) | ||
| hir::Node::AssocItemConstraint(_) | ||
| hir::Node::TraitRef(_) | ||
| hir::Node::Pat(_) | ||
| hir::Node::PatField(_) | ||
| hir::Node::LetStmt(_) | ||
| hir::Node::Synthetic | ||
| hir::Node::Err(_) | ||
| hir::Node::Ctor(_) | ||
| hir::Node::Lifetime(_) | ||
| hir::Node::GenericParam(_) | ||
| hir::Node::Crate(_) | ||
| hir::Node::Infer(_) | ||
| hir::Node::WhereBoundPredicate(_) | ||
| hir::Node::ArrayLenInfer(_) | ||
| hir::Node::PreciseCapturingNonLifetimeArg(_) | ||
| hir::Node::OpaqueTy(_) => { | ||
unreachable!("no sub-expr expected for {parent_node:?}") | ||
} | ||
} | ||
} | ||
|
||
/// Whether this pattern constitutes a read of value of the scrutinee that | ||
/// it is matching against. This is used to determine whether we should | ||
compiler-errors marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// perform `NeverToAny` coercions. | ||
/// | ||
/// See above for the nuances of what happens when this returns true. | ||
pub(super) fn pat_guaranteed_to_constitute_read_for_never(&self, pat: &hir::Pat<'_>) -> bool { | ||
match pat.kind { | ||
// Does not constitute a read. | ||
hir::PatKind::Wild => false, | ||
|
||
// This is unnecessarily restrictive when the pattern that doesn't | ||
// constitute a read is unreachable. | ||
// | ||
// For example `match *never_ptr { value => {}, _ => {} }` or | ||
// `match *never_ptr { _ if false => {}, value => {} }`. | ||
// | ||
// It is however fine to be restrictive here; only returning `true` | ||
// can lead to unsoundness. | ||
hir::PatKind::Or(subpats) => { | ||
subpats.iter().all(|pat| self.pat_guaranteed_to_constitute_read_for_never(pat)) | ||
} | ||
|
||
// Does constitute a read, since it is equivalent to a discriminant read. | ||
hir::PatKind::Never => true, | ||
|
||
// All of these constitute a read, or match on something that isn't `!`, | ||
// which would require a `NeverToAny` coercion. | ||
Comment on lines
+428
to
+429
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is that right? A binding pattern All the other ones look good to me. I am unsure whether it's possible to write a test for that rn as I think we don't coerce there rn? I guess please extend the tests/ui/never_type/diverging-place-match.rs to also test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, and I can add it to the issue I opened too. |
||
hir::PatKind::Binding(_, _, _, _) | ||
| hir::PatKind::Struct(_, _, _) | ||
| hir::PatKind::TupleStruct(_, _, _) | ||
| hir::PatKind::Path(_) | ||
| hir::PatKind::Tuple(_, _) | ||
| hir::PatKind::Box(_) | ||
| hir::PatKind::Ref(_, _) | ||
| hir::PatKind::Deref(_) | ||
| hir::PatKind::Lit(_) | ||
| hir::PatKind::Range(_, _, _) | ||
| hir::PatKind::Slice(_, _, _) | ||
| hir::PatKind::Err(_) => true, | ||
} | ||
} | ||
|
||
#[instrument(skip(self, expr), level = "debug")] | ||
fn check_expr_kind( | ||
&self, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, that means we can end up with
can_coerce
returning false and then coercing failing. I'd have to look at how this is used to check whether it may be a problem (or you tell me)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well firstly,
can_coerce
is called on arbitrary types that are not associated with values or places, so getting this 100% right is literally intractible.can_coerce
is used likecan_eq
, where it is heuristic. It should never be used for correctness, since it also records no adjustments and has no side-effects.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can_coerce
is only used on the error path or in clippy, though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I wanted to know whether we use it anywhere to decide "yes, this is a coercion" only to later error. i.e. whether this results in unintended errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I checked and there is none. Only in suggestion code or clippy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR implements a heuristic which disables two things that are currently being performed on the HIR when we have expressions that involve place-like expressions that point to
!
. Specifically, it will:NeverToAny
coercion we implicitly insert.We do those IF the expression is a place-like expression (locals, statics, field projections, index operations, and deref operations), and if the parent expression is either:
And finally, a pattern currently is considered to constitute a read UNLESS it is a wildcard or a OR of wildcards. All other patterns are considered currently to constitute a read. Specifically, because
NeverToAny
is a coercion performed on a value and not a place,Struct { .. }
on a!
type must be a coercion currently. This is already considered UB by miri.That means it does not affect the preexisting UB in this case:
Even though it's likely up for debate, it almost certainly causes inference changes which I do NOT want to fix in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "not so egregiously unsound" case is basically the last example I gave, since that is arguably not even unsound but just regular UB because users already need to accept that they're doing sketchy stuff by wrapping their code in
unsafe {}
, and the fact that the coercion there is implicitly occurring is kinda just 🤷.Recall that I'm primarily motivated by trying to make
&raw *ptr
always safe, and none of this affects that. The only stuff that's "up for debate" here is just the semantics surrounding pattern matching, which I think are probably incredibly rare, already more UB than they are than after this PR, and are easily refined later.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do want to apologize though, my earlier comment here was a bit dramatic since I have (not the fault of the reviewers, of course) been a bit overwhelmed by the process of carrying out this work with fixing never+place exprs in the HIR, especially since my original desire was just to fix the simplest cases of this (e.g. the AddrOf case).
I do agree that there is a lot of design space for how we treat patterns, and I wasn't clear that when I said I didn't really want to further work on this, I really meant I didn't want to tweak this PR in cases that are presumably more "up for debate" like struct patterns that do not read, especially given how the compiler treats them on nightly.
I can certainly document where this PR ends up if and when it does actually land, though I am not too certain where that documentation would actually live.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the frustration of starting with something seemingly small and then having it balloon to way more than that. :)
Thanks for the summary! If we have no better idea, a good place to document this would be an issue where we track figuring out the intended semantics and what it would take to adjust the compiler to implement those semantics.
let Struct { .. } = *never_ptr;
is a wild case -- yes it needs the!
as a value, I agree, so if we accept it it should be UB. (IMO we should just reject such code but oh well, probably too late for that. Maybe we can at least lint against it if the place on the RHS is an unsafe place, i.e. based on a raw ptr or union field. Anyway that should probably go into the issue mentioned above.)OTOH if
ptr: StructNever
withstruct StructNever(!)
, thenlet StructNever { .. } = *ptr
should not be UB. But here*ptr
does not have type!
and IIUC the special behavior that triggers the coercion is specific to!
, it does not apply to all uninhabited types?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. We only perform this behavior of
!
and no other uninhabited types. That's partly why I renamed this function to..._guaranteed_to_constitute_read_for_never
, since it only really is tuned for!
.