Skip to content

Commit

Permalink
Check for uninhabited types in typeck
Browse files Browse the repository at this point in the history
  • Loading branch information
camsteffen authored and cjgillot committed Apr 7, 2024
1 parent 5ba9e4b commit 827be96
Show file tree
Hide file tree
Showing 45 changed files with 606 additions and 315 deletions.
2 changes: 2 additions & 0 deletions compiler/rustc_hir_typeck/src/diverges.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use rustc_hir::HirId;
use rustc_span::Span;

use std::{cmp, ops};
Expand Down Expand Up @@ -72,5 +73,6 @@ impl Diverges {
pub enum DivergeReason {
AllArmsDiverge,
NeverPattern,
UninhabitedExpr(HirId),
Other,
}
79 changes: 74 additions & 5 deletions compiler/rustc_hir_typeck/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,40 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
// diverging expression (e.g. it arose from desugaring of `try { return }`),
// we skip issuing a warning because it is autogenerated code.
ExprKind::Call(..) if expr.span.is_desugaring(DesugaringKind::TryBlock) => {}
ExprKind::Call(callee, _) => self.warn_if_unreachable(expr.hir_id, callee.span, "call"),
ExprKind::Call(callee, _) => {
let emit_warning = if let ExprKind::Path(ref qpath) = callee.kind {
// Do not emit a warning for a call to a constructor.
let res = self.typeck_results.borrow().qpath_res(qpath, callee.hir_id);
!matches!(res, Res::Def(DefKind::Ctor(..), _))
} else {
true
};
if emit_warning {
self.warn_if_unreachable(expr.hir_id, callee.span, "call")
}
}
ExprKind::MethodCall(segment, ..) => {
self.warn_if_unreachable(expr.hir_id, segment.ident.span, "call")
}
// allow field access when the struct and the field are both uninhabited
ExprKind::Field(..)
if matches!(
self.diverges.get(),
Diverges::Always(DivergeReason::UninhabitedExpr(_), _)
) && self.ty_is_uninhabited(ty) => {}
_ => 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() {
self.diverges
.set(self.diverges.get() | Diverges::Always(DivergeReason::Other, expr.span));
if !self.diverges.get().is_always() {
if ty.is_never() {
// Any expression that produces a value of type `!` must have diverged.
self.diverges.set(Diverges::Always(DivergeReason::Other, expr.span));
} else if expr_may_be_uninhabited(expr) && self.ty_is_uninhabited(ty) {
// This expression produces a value of uninhabited type.
// This means it has diverged somehow.
self.diverges
.set(Diverges::Always(DivergeReason::UninhabitedExpr(expr.hir_id), expr.span));
}
}

// Record the type, which applies it effects.
Expand All @@ -271,6 +294,13 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
ty
}

fn ty_is_uninhabited(&self, ty: Ty<'tcx>) -> bool {
let ty = self.resolve_vars_if_possible(ty);
// Freshen the type as `is_inhabited_from` may call a query on `ty`.
let ty = self.freshen(ty);
!ty.is_inhabited_from(self.tcx, self.parent_module, self.param_env)
}

#[instrument(skip(self, expr), level = "debug")]
fn check_expr_kind(
&self,
Expand Down Expand Up @@ -3451,3 +3481,42 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
self.tcx.types.usize
}
}

fn expr_may_be_uninhabited(expr: &hir::Expr<'_>) -> bool {
match expr.kind {
ExprKind::Call(..)
| ExprKind::MethodCall(..)
| ExprKind::Cast(..)
| ExprKind::Unary(hir::UnOp::Deref, _)
| ExprKind::Field(..)
| ExprKind::Path(..)
| ExprKind::Struct(..) => true,
ExprKind::ConstBlock(..)
| ExprKind::Array(..)
| ExprKind::Tup(..)
| ExprKind::Binary(..)
| ExprKind::Unary(hir::UnOp::Neg | hir::UnOp::Not, _)
| ExprKind::Lit(..)
| ExprKind::Type(..)
| ExprKind::DropTemps(..)
| ExprKind::OffsetOf(..)
| ExprKind::Let(..)
| ExprKind::If(..)
| ExprKind::Loop(..)
| ExprKind::Match(..)
| ExprKind::Closure(..)
| ExprKind::Block(..)
| ExprKind::Assign(..)
| ExprKind::AssignOp(..)
| ExprKind::Index(..)
| ExprKind::AddrOf(..)
| ExprKind::Break(..)
| ExprKind::Continue(..)
| ExprKind::Ret(..)
| ExprKind::Become(..)
| ExprKind::InlineAsm(..)
| ExprKind::Repeat(..)
| ExprKind::Yield(..)
| ExprKind::Err(_) => false,
}
}
25 changes: 22 additions & 3 deletions compiler/rustc_hir_typeck/src/fn_ctxt/_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use rustc_trait_selection::traits::{
self, NormalizeExt, ObligationCauseCode, ObligationCtxt, StructurallyNormalizeExt,
};

use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::slice;

Expand All @@ -63,16 +64,34 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {

self.diverges.set(Diverges::WarnedAlways);

if matches!(reason, DivergeReason::UninhabitedExpr(_)) {
if let Some(impl_of) = self.tcx.impl_of_method(self.body_id.to_def_id()) {
if self.tcx.has_attr(impl_of, sym::automatically_derived) {
// Built-in derives are generated before typeck,
// so they may contain unreachable code if there are uninhabited types
return;
}
}
}

debug!("warn_if_unreachable: id={:?} span={:?} kind={}", id, span, kind);

let msg = format!("unreachable {kind}");
self.tcx().node_span_lint(lint::builtin::UNREACHABLE_CODE, id, span, msg.clone(), |lint| {
let label = match reason {
let label: Cow<'_, _> = match reason {
DivergeReason::AllArmsDiverge => {
"any code following this `match` expression is unreachable, as all arms diverge"
.into()
}
DivergeReason::NeverPattern => {
"any code following a never pattern is unreachable".into()
}
DivergeReason::NeverPattern => "any code following a never pattern is unreachable",
DivergeReason::Other => "any code following this expression is unreachable",
DivergeReason::UninhabitedExpr(hir_id) => format!(
"this expression has type `{}`, which is uninhabited",
self.typeck_results.borrow().node_type(hir_id)
)
.into(),
DivergeReason::Other => "any code following this expression is unreachable".into(),
};
lint.span_label(span, msg).span_label(orig_span, label);
})
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct FnCtxt<'a, 'tcx> {
/// eventually).
pub(super) param_env: ty::ParamEnv<'tcx>,

pub(super) parent_module: DefId,

/// If `Some`, this stores coercion information for returned
/// expressions. If `None`, this is in a context where return is
/// inappropriate, such as a const expression.
Expand Down Expand Up @@ -127,6 +129,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
FnCtxt {
body_id,
param_env,
parent_module: root_ctxt.tcx.parent_module_from_def_id(body_id).to_def_id(),
ret_coercion: None,
ret_coercion_span: Cell::new(None),
coroutine_types: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,7 @@ impl<'tcx> InhabitedPredicate<'tcx> {
Self::NotInModule(id) => in_module(id).map(|in_mod| !in_mod),
// `t` may be a projection, for which `inhabited_predicate` returns a `GenericType`. As
// we have a param_env available, we can do better.
Self::GenericType(t) => {
let normalized_pred = tcx
.try_normalize_erasing_regions(param_env, t)
.map_or(self, |t| t.inhabited_predicate(tcx));
match normalized_pred {
// We don't have more information than we started with, so consider inhabited.
Self::GenericType(_) => Ok(true),
pred => {
// A type which is cyclic when monomorphized can happen here since the
// layout error would only trigger later. See e.g. `tests/ui/sized/recursive-type-2.rs`.
if eval_stack.contains(&t) {
return Ok(true); // Recover; this will error later.
}
eval_stack.push(t);
let ret =
pred.apply_inner(tcx, param_env, eval_stack, in_module, reveal_opaque);
eval_stack.pop();
ret
}
}
}
Self::GenericType(_) => Ok(true),
Self::OpaqueType(key) => match reveal_opaque(key) {
// Unknown opaque is assumed inhabited.
None => Ok(true),
Expand Down
71 changes: 13 additions & 58 deletions compiler/rustc_passes/src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ use rustc_hir::intravisit::{self, Visitor};
use rustc_hir::{Expr, HirId, HirIdMap, HirIdSet};
use rustc_index::IndexVec;
use rustc_middle::query::Providers;
use rustc_middle::ty::{self, RootVariableMinCaptureList, Ty, TyCtxt};
use rustc_middle::ty::{self, RootVariableMinCaptureList, TyCtxt};
use rustc_session::lint;
use rustc_span::symbol::{kw, sym, Symbol};
use rustc_span::{BytePos, Span};
Expand All @@ -118,8 +118,8 @@ rustc_index::newtype_index! {
#[derive(Copy, Clone, PartialEq, Debug)]
enum LiveNodeKind {
UpvarNode(Span),
ExprNode(Span, HirId),
VarDefNode(Span, HirId),
ExprNode(Span),
VarDefNode(Span),
ClosureNode,
ExitNode,
ErrNode,
Expand All @@ -129,8 +129,8 @@ fn live_node_kind_to_string(lnk: LiveNodeKind, tcx: TyCtxt<'_>) -> String {
let sm = tcx.sess.source_map();
match lnk {
UpvarNode(s) => format!("Upvar node [{}]", sm.span_to_diagnostic_string(s)),
ExprNode(s, _) => format!("Expr node [{}]", sm.span_to_diagnostic_string(s)),
VarDefNode(s, _) => format!("Var def node [{}]", sm.span_to_diagnostic_string(s)),
ExprNode(s) => format!("Expr node [{}]", sm.span_to_diagnostic_string(s)),
VarDefNode(s) => format!("Var def node [{}]", sm.span_to_diagnostic_string(s)),
ClosureNode => "Closure node".to_owned(),
ExitNode => "Exit node".to_owned(),
ErrNode => "Error node".to_owned(),
Expand Down Expand Up @@ -331,7 +331,7 @@ impl<'tcx> IrMaps<'tcx> {
let shorthand_field_ids = self.collect_shorthand_field_ids(pat);

pat.each_binding(|_, hir_id, _, ident| {
self.add_live_node_for_node(hir_id, VarDefNode(ident.span, hir_id));
self.add_live_node_for_node(hir_id, VarDefNode(ident.span));
self.add_variable(Local(LocalInfo {
id: hir_id,
name: ident.name,
Expand All @@ -345,7 +345,7 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
fn visit_local(&mut self, local: &'tcx hir::LetStmt<'tcx>) {
self.add_from_pat(local.pat);
if local.els.is_some() {
self.add_live_node_for_node(local.hir_id, ExprNode(local.span, local.hir_id));
self.add_live_node_for_node(local.hir_id, ExprNode(local.span));
}
intravisit::walk_local(self, local);
}
Expand Down Expand Up @@ -377,13 +377,13 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
hir::ExprKind::Path(hir::QPath::Resolved(_, path)) => {
debug!("expr {}: path that leads to {:?}", expr.hir_id, path.res);
if let Res::Local(_var_hir_id) = path.res {
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
}
}
hir::ExprKind::Closure(closure) => {
// Interesting control flow (for loops can contain labeled
// breaks or continues)
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));

// Make a live_node for each mentioned variable, with the span
// being the location that the variable is used. This results
Expand All @@ -409,15 +409,15 @@ impl<'tcx> Visitor<'tcx> for IrMaps<'tcx> {
| hir::ExprKind::Match(..)
| hir::ExprKind::Loop(..)
| hir::ExprKind::Yield(..) => {
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
}
hir::ExprKind::Binary(op, ..) if op.node.is_lazy() => {
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
}

// Inline assembly may contain labels.
hir::ExprKind::InlineAsm(asm) if asm.contains_label() => {
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span, expr.hir_id));
self.add_live_node_for_node(expr.hir_id, ExprNode(expr.span));
intravisit::walk_expr(self, expr);
}

Expand Down Expand Up @@ -1297,52 +1297,7 @@ impl<'a, 'tcx> Liveness<'a, 'tcx> {
fn check_is_ty_uninhabited(&mut self, expr: &Expr<'_>, succ: LiveNode) -> LiveNode {
let ty = self.typeck_results.expr_ty(expr);
let m = self.ir.tcx.parent_module(expr.hir_id).to_def_id();
if ty.is_inhabited_from(self.ir.tcx, m, self.param_env) {
return succ;
}
match self.ir.lnks[succ] {
LiveNodeKind::ExprNode(succ_span, succ_id) => {
self.warn_about_unreachable(expr.span, ty, succ_span, succ_id, "expression");
}
LiveNodeKind::VarDefNode(succ_span, succ_id) => {
self.warn_about_unreachable(expr.span, ty, succ_span, succ_id, "definition");
}
_ => {}
};
self.exit_ln
}

fn warn_about_unreachable<'desc>(
&mut self,
orig_span: Span,
orig_ty: Ty<'tcx>,
expr_span: Span,
expr_id: HirId,
descr: &'desc str,
) {
if !orig_ty.is_never() {
// Unreachable code warnings are already emitted during type checking.
// However, during type checking, full type information is being
// calculated but not yet available, so the check for diverging
// expressions due to uninhabited result types is pretty crude and
// only checks whether ty.is_never(). Here, we have full type
// information available and can issue warnings for less obviously
// uninhabited types (e.g. empty enums). The check above is used so
// that we do not emit the same warning twice if the uninhabited type
// is indeed `!`.

self.ir.tcx.emit_node_span_lint(
lint::builtin::UNREACHABLE_CODE,
expr_id,
expr_span,
errors::UnreachableDueToUninhabited {
expr: expr_span,
orig: orig_span,
descr,
ty: orig_ty,
},
);
}
if ty.is_inhabited_from(self.ir.tcx, m, self.param_env) { succ } else { self.exit_ln }
}
}

Expand Down
Loading

0 comments on commit 827be96

Please sign in to comment.