Skip to content

Commit

Permalink
Auto merge of #68350 - Aaron1011:its-daylight-saving-time, r=<try>
Browse files Browse the repository at this point in the history
Add lint for never type regressions

Fixes #67225

Tl;DR: This PR introduces a lint that detects the 'bad' never-type fallback in `objc` (which results in U.B.), while allowing safe fallback to occur without a warning.

See https://hackmd.io/@FohtAO04T92wF-8_TVATYQ/SJ0vcjyWL for some background on never-type fallback.

### The problem

We want to reject "bad" code like this:

```rust
fn unconstrained_return<T>() -> Result<T, String> {
    let ffi: fn() -> T = transmute(some_pointer);
    Ok(ffi())
}
fn foo() {
    match unconstrained_return::<_>() {
        Ok(x) => x,  // `x` has type `_`, which is unconstrained
        Err(s) => panic!(s),  // … except for unifying with the type of `panic!()`
        // so that both `match` arms have the same type.
        // Therefore `_` resolves to `!` and we "return" an `Ok(!)` value.
    };
}
```

in which enabling never-type fallback can cause undefined behavior.

However, we want to allow "good" code like this:

```rust
struct E;
impl From<!> for E {
    fn from(x: !) -> E { x }
}
fn foo(never: !) {
    <E as From<_>>::from(never);
}

fn main() {}
```

which relies on never-type fallback being enabled, but is perfectly safe.

### The solution

The key difference between these two examples lies in how the result of never-type fallback is used. In the first example, we end up inferring the generic parameter of `unconstrained_return` to be `!`. In the second example, we still infer a generic parameter to be `!` (`Box::<!>::new(!)`), but we also pass an uninhabited parameter to the function.

Another way of looking at this is that the call to `unconstrained_return` is **potentially live* - none of its arguments are uninhabited, so we might (and in fact, do) end up actually executing the call at runtime.

In the second example, `Box::new()` has an uninhabited argument (the `!` type). This means that this call is **definitely dead** - since the `!` type can never be instantiated, it's impossible for the call to every be executed.

This forms the basis for the check. For each method call, we check the following:

1. Did the generic arguments have unconstrained type variables prior to fallback?
2. Did any of the generic arguments become uninhabited after fallback?
3. Are all of the method arguments inhabited?

If the answer to all of these is *yes*, we emit an error. I've left extensive comments in the code describing how this is accomplished.

These conditions ensure that we don't error on the `Box` and `From<!>` examples, while still erroring on the bad `objc` code.

### Further notes

You can test out this branch with the original bad `objc` code as follows:

1. Clone `https://github.com/Aaron1011/rust-objc`
2. Checkout the `bad-fallback` branch.
3. With a local rustc toolchain built from this branch, run `cargo build --example example`
4. Note that you get an error due to an unconstrained return type

### Unresolved questions

1. This lint only checks method calls. I believe this is sufficient to catch any undefined behavior caused by fallback changes. Since the introduced undefined behavior relies on actually 'producing' a `!` type instance, the user must be doing something 'weird' (calling `transmute` or some other intrinsic). I don't think it's possible to trigger this without *some* kind of intrinsic call - however, I'm not 100% certain.

2. This lint requires us to perform extra work during the type-checking of every single method. This is not ideal - however, changing this would have required significant refactoring to method type-checking. It would be a good idea to due to a perf run to see what kind of impact this has, and it another approach will be required.

3. This 'lint' is currently a hard error. I believe it should always be possible to fix this error by adding explicit type annotations *somewhere* (though in the `obj` case, this may be in the caller of a macro). Unfortunately, I think actually emitting any kind of suggestion to the user will be extremely difficult. Hopefully, this error is so rare that the lack of suggestion isn't a problem. If users are running into this with any frequency, I think we'll need a different approach.

4. If this PR is accepted, I see two ways of rolling this out:

1. If the bad `objc` crate is the only crate known to be affected, we could potentially go from no warning/lint to a hard error in a single release (coupled enabling never-type fallback0.
2. If we're worried that this could break a lot of crates, we could make this into a future compatibility lint. At some point in the future, we could enable never-type fallback while simultaneously making this a hard error.

What we should **not** do is make the never-type fallback changes without making this lint (or whatever lint ends up getting accepted) into a hard error. A lint, even a deny-by-default one, would be insufficient, as we would run a serious risk introducing undefined behavior without any kind of explicit acknowledgment from the user.
  • Loading branch information
bors committed Jan 18, 2020
2 parents 779f85b + d4bd422 commit 873f3b0
Show file tree
Hide file tree
Showing 9 changed files with 695 additions and 2 deletions.
119 changes: 117 additions & 2 deletions src/librustc_typeck/check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ mod expr;
mod generator_interior;
pub mod intrinsic;
pub mod method;
mod never_compat;
mod op;
mod pat;
mod regionck;
Expand Down Expand Up @@ -135,6 +136,7 @@ use syntax::util::parser::ExprPrecedence;

use rustc_error_codes::*;

use std::borrow::Cow;
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::cmp;
use std::collections::hash_map::Entry;
Expand Down Expand Up @@ -254,6 +256,14 @@ pub struct Inherited<'a, 'tcx> {
/// not clear.
implicit_region_bound: Option<ty::Region<'tcx>>,

/// Maps each expression with a generic path
/// (e.g. `foo::<u8>()` to `InferredPath` containing
/// additional information used by `NeverCompatHandler`.
///
/// Each entry in this map is gradually filled in during typecheck,
/// as the information we need is not available all at once.
inferred_paths: RefCell<FxHashMap<hir::HirId, InferredPath<'tcx>>>,

body_id: Option<hir::BodyId>,
}

Expand Down Expand Up @@ -619,6 +629,48 @@ pub struct FnCtxt<'a, 'tcx> {
inh: &'a Inherited<'a, 'tcx>,
}

/// Stores additional data about a generic path
/// containing inference variables (e.g. `my_method::<_, u8>(bar)`).
/// This is used by `NeverCompatHandler` to inspect
/// all method calls that contain inference variables.
///
/// This struct is a little strange, in that its data
/// is filled in from two different places in typecheck.
/// Thw two `Option` fields are written by `check_argument_types`
/// and `instantiate_value_path`, since neither method
/// has all of the information it needs.
#[derive(Clone, Debug)]
struct InferredPath<'tcx> {
/// The span of the corresponding expression.
span: Span,
/// The type of this path. For method calls,
/// this is a `ty::FnDef`
ty: Option<Ty<'tcx>>,
/// The types of the arguments (*not* generic substs)
/// provided to this path, if it represents a method
/// call. For example, `foo(true, 25)` would have
/// types `[bool, i32]`. If this path does not
/// correspond to a method, then this will be `None`
///
/// This is a `Cow` rather than a `Vec` or slice
/// to accommodate `check_argument_types`, which may
/// be called with either an interned slice or a Vec.
/// A `Cow` lets us avoid unecessary interning
/// and Vec construction, since we just need to iterate
/// over this
args: Option<Cow<'tcx, [Ty<'tcx>]>>,
/// The unresolved inference variables for each
/// generic substs. Each entry in the outer vec
/// corresponds to a generic substs in the function.
///
/// For example, suppose we have the function
/// `fn foo<T, V> (){ ... }`.
///
/// The method call `foo::<MyStruct<_#0t, #1t>, true>>()`
/// will have an `unresolved_vars` of `[[_#0t, _#1t], []]`
unresolved_vars: Vec<Vec<Ty<'tcx>>>,
}

impl<'a, 'tcx> Deref for FnCtxt<'a, 'tcx> {
type Target = Inherited<'a, 'tcx>;
fn deref(&self) -> &Self::Target {
Expand Down Expand Up @@ -685,6 +737,7 @@ impl Inherited<'a, 'tcx> {
opaque_types: RefCell::new(Default::default()),
opaque_types_vars: RefCell::new(Default::default()),
implicit_region_bound,
inferred_paths: RefCell::new(Default::default()),
body_id,
}
}
Expand Down Expand Up @@ -1053,6 +1106,7 @@ fn typeck_tables_of_with_fallback<'tcx>(
// All type checking constraints were added, try to fallback unsolved variables.
fcx.select_obligations_where_possible(false, |_| {});
let mut fallback_has_occurred = false;
let never_compat = never_compat::NeverCompatHandler::pre_fallback(&fcx);

// We do fallback in two passes, to try to generate
// better error messages.
Expand Down Expand Up @@ -1095,6 +1149,8 @@ fn typeck_tables_of_with_fallback<'tcx>(
// See if we can make any more progress.
fcx.select_obligations_where_possible(fallback_has_occurred, |_| {});

never_compat.post_fallback(&fcx);

// Even though coercion casts provide type hints, we check casts after fallback for
// backwards compatibility. This makes fallback a stronger type hint than a cast coercion.
fcx.check_casts();
Expand Down Expand Up @@ -3618,7 +3674,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
self.check_argument_types(
sp,
expr,
&err_inputs[..],
err_inputs,
&[],
args_no_rcvr,
false,
Expand Down Expand Up @@ -3726,13 +3782,39 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
&self,
sp: Span,
expr: &'tcx hir::Expr<'tcx>,
fn_inputs: &[Ty<'tcx>],
fn_inputs: impl Into<Cow<'tcx, [Ty<'tcx>]>>,
expected_arg_tys: &[Ty<'tcx>],
args: &'tcx [hir::Expr<'tcx>],
c_variadic: bool,
tuple_arguments: TupleArgumentsFlag,
def_span: Option<Span>,
) {
let fn_inputs = fn_inputs.into();
debug!("check_argument_types: storing arguments for expr {:?}", expr);
// We now have the arguments types available for this msthod call,
// so store them in the `inferred_paths` entry for this method call.
// We set `ty` as `None` if we are the first to access the entry
// for this method, and leave it untouched otherwise.
match self.inferred_paths.borrow_mut().entry(expr.hir_id) {
Entry::Vacant(e) => {
debug!("check_argument_types: making new entry for types {:?}", fn_inputs);
e.insert(InferredPath {
span: sp,
ty: None,
args: Some(fn_inputs.clone()),
unresolved_vars: vec![],
});
}
Entry::Occupied(mut e) => {
debug!(
"check_argument_types: modifying existing {:?} with types {:?}",
e.get(),
fn_inputs
);
e.get_mut().args = Some(fn_inputs.clone());
}
}

let tcx = self.tcx;
// Grab the argument types, supplying fresh type variables
// if the wrong number of arguments were supplied
Expand Down Expand Up @@ -5419,6 +5501,39 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
// the referenced item.
let ty_substituted = self.instantiate_type_scheme(span, &substs, &ty);

if ty_substituted.has_infer_types() {
debug!(
"instantiate_value_path: saving path with infer: ({:?}, {:?})",
span, ty_substituted
);
let parent_id = tcx.hir().get_parent_node(hir_id);
let parent = tcx.hir().get(parent_id);
match parent {
Node::Expr(hir::Expr { span: p_span, kind: ExprKind::Call(..), .. })
| Node::Expr(hir::Expr { span: p_span, kind: ExprKind::MethodCall(..), .. }) => {
// Fill in the type for our parent expression. This might not be
// a method call - if it is, the argumetns will be filled in by
// `check_argument_types`
match self.inferred_paths.borrow_mut().entry(parent_id) {
Entry::Vacant(e) => {
debug!("instantiate_value_path: inserting new path");
e.insert(InferredPath {
span: *p_span,
ty: Some(ty_substituted),
args: None,
unresolved_vars: vec![],
});
}
Entry::Occupied(mut e) => {
debug!("instantiate_value_path: updating existing path {:?}", e.get());
e.get_mut().ty = Some(ty_substituted);
}
}
}
_ => {}
}
}

if let Some(UserSelfTy { impl_def_id, self_ty }) = user_self_ty {
// In the case of `Foo<T>::method` and `<Foo<T>>::method`, if `method`
// is inherent, there is no `Self` parameter; instead, the impl needs
Expand Down
Loading

0 comments on commit 873f3b0

Please sign in to comment.