Skip to content

Commit 873f3b0

Browse files
committed
Auto merge of #68350 - Aaron1011:its-daylight-saving-time, r=<try>
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.
2 parents 779f85b + d4bd422 commit 873f3b0

File tree

9 files changed

+695
-2
lines changed

9 files changed

+695
-2
lines changed

src/librustc_typeck/check/mod.rs

+117-2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mod expr;
8080
mod generator_interior;
8181
pub mod intrinsic;
8282
pub mod method;
83+
mod never_compat;
8384
mod op;
8485
mod pat;
8586
mod regionck;
@@ -135,6 +136,7 @@ use syntax::util::parser::ExprPrecedence;
135136

136137
use rustc_error_codes::*;
137138

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

259+
/// Maps each expression with a generic path
260+
/// (e.g. `foo::<u8>()` to `InferredPath` containing
261+
/// additional information used by `NeverCompatHandler`.
262+
///
263+
/// Each entry in this map is gradually filled in during typecheck,
264+
/// as the information we need is not available all at once.
265+
inferred_paths: RefCell<FxHashMap<hir::HirId, InferredPath<'tcx>>>,
266+
257267
body_id: Option<hir::BodyId>,
258268
}
259269

@@ -619,6 +629,48 @@ pub struct FnCtxt<'a, 'tcx> {
619629
inh: &'a Inherited<'a, 'tcx>,
620630
}
621631

632+
/// Stores additional data about a generic path
633+
/// containing inference variables (e.g. `my_method::<_, u8>(bar)`).
634+
/// This is used by `NeverCompatHandler` to inspect
635+
/// all method calls that contain inference variables.
636+
///
637+
/// This struct is a little strange, in that its data
638+
/// is filled in from two different places in typecheck.
639+
/// Thw two `Option` fields are written by `check_argument_types`
640+
/// and `instantiate_value_path`, since neither method
641+
/// has all of the information it needs.
642+
#[derive(Clone, Debug)]
643+
struct InferredPath<'tcx> {
644+
/// The span of the corresponding expression.
645+
span: Span,
646+
/// The type of this path. For method calls,
647+
/// this is a `ty::FnDef`
648+
ty: Option<Ty<'tcx>>,
649+
/// The types of the arguments (*not* generic substs)
650+
/// provided to this path, if it represents a method
651+
/// call. For example, `foo(true, 25)` would have
652+
/// types `[bool, i32]`. If this path does not
653+
/// correspond to a method, then this will be `None`
654+
///
655+
/// This is a `Cow` rather than a `Vec` or slice
656+
/// to accommodate `check_argument_types`, which may
657+
/// be called with either an interned slice or a Vec.
658+
/// A `Cow` lets us avoid unecessary interning
659+
/// and Vec construction, since we just need to iterate
660+
/// over this
661+
args: Option<Cow<'tcx, [Ty<'tcx>]>>,
662+
/// The unresolved inference variables for each
663+
/// generic substs. Each entry in the outer vec
664+
/// corresponds to a generic substs in the function.
665+
///
666+
/// For example, suppose we have the function
667+
/// `fn foo<T, V> (){ ... }`.
668+
///
669+
/// The method call `foo::<MyStruct<_#0t, #1t>, true>>()`
670+
/// will have an `unresolved_vars` of `[[_#0t, _#1t], []]`
671+
unresolved_vars: Vec<Vec<Ty<'tcx>>>,
672+
}
673+
622674
impl<'a, 'tcx> Deref for FnCtxt<'a, 'tcx> {
623675
type Target = Inherited<'a, 'tcx>;
624676
fn deref(&self) -> &Self::Target {
@@ -685,6 +737,7 @@ impl Inherited<'a, 'tcx> {
685737
opaque_types: RefCell::new(Default::default()),
686738
opaque_types_vars: RefCell::new(Default::default()),
687739
implicit_region_bound,
740+
inferred_paths: RefCell::new(Default::default()),
688741
body_id,
689742
}
690743
}
@@ -1053,6 +1106,7 @@ fn typeck_tables_of_with_fallback<'tcx>(
10531106
// All type checking constraints were added, try to fallback unsolved variables.
10541107
fcx.select_obligations_where_possible(false, |_| {});
10551108
let mut fallback_has_occurred = false;
1109+
let never_compat = never_compat::NeverCompatHandler::pre_fallback(&fcx);
10561110

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

1152+
never_compat.post_fallback(&fcx);
1153+
10981154
// Even though coercion casts provide type hints, we check casts after fallback for
10991155
// backwards compatibility. This makes fallback a stronger type hint than a cast coercion.
11001156
fcx.check_casts();
@@ -3618,7 +3674,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
36183674
self.check_argument_types(
36193675
sp,
36203676
expr,
3621-
&err_inputs[..],
3677+
err_inputs,
36223678
&[],
36233679
args_no_rcvr,
36243680
false,
@@ -3726,13 +3782,39 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
37263782
&self,
37273783
sp: Span,
37283784
expr: &'tcx hir::Expr<'tcx>,
3729-
fn_inputs: &[Ty<'tcx>],
3785+
fn_inputs: impl Into<Cow<'tcx, [Ty<'tcx>]>>,
37303786
expected_arg_tys: &[Ty<'tcx>],
37313787
args: &'tcx [hir::Expr<'tcx>],
37323788
c_variadic: bool,
37333789
tuple_arguments: TupleArgumentsFlag,
37343790
def_span: Option<Span>,
37353791
) {
3792+
let fn_inputs = fn_inputs.into();
3793+
debug!("check_argument_types: storing arguments for expr {:?}", expr);
3794+
// We now have the arguments types available for this msthod call,
3795+
// so store them in the `inferred_paths` entry for this method call.
3796+
// We set `ty` as `None` if we are the first to access the entry
3797+
// for this method, and leave it untouched otherwise.
3798+
match self.inferred_paths.borrow_mut().entry(expr.hir_id) {
3799+
Entry::Vacant(e) => {
3800+
debug!("check_argument_types: making new entry for types {:?}", fn_inputs);
3801+
e.insert(InferredPath {
3802+
span: sp,
3803+
ty: None,
3804+
args: Some(fn_inputs.clone()),
3805+
unresolved_vars: vec![],
3806+
});
3807+
}
3808+
Entry::Occupied(mut e) => {
3809+
debug!(
3810+
"check_argument_types: modifying existing {:?} with types {:?}",
3811+
e.get(),
3812+
fn_inputs
3813+
);
3814+
e.get_mut().args = Some(fn_inputs.clone());
3815+
}
3816+
}
3817+
37363818
let tcx = self.tcx;
37373819
// Grab the argument types, supplying fresh type variables
37383820
// if the wrong number of arguments were supplied
@@ -5419,6 +5501,39 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
54195501
// the referenced item.
54205502
let ty_substituted = self.instantiate_type_scheme(span, &substs, &ty);
54215503

5504+
if ty_substituted.has_infer_types() {
5505+
debug!(
5506+
"instantiate_value_path: saving path with infer: ({:?}, {:?})",
5507+
span, ty_substituted
5508+
);
5509+
let parent_id = tcx.hir().get_parent_node(hir_id);
5510+
let parent = tcx.hir().get(parent_id);
5511+
match parent {
5512+
Node::Expr(hir::Expr { span: p_span, kind: ExprKind::Call(..), .. })
5513+
| Node::Expr(hir::Expr { span: p_span, kind: ExprKind::MethodCall(..), .. }) => {
5514+
// Fill in the type for our parent expression. This might not be
5515+
// a method call - if it is, the argumetns will be filled in by
5516+
// `check_argument_types`
5517+
match self.inferred_paths.borrow_mut().entry(parent_id) {
5518+
Entry::Vacant(e) => {
5519+
debug!("instantiate_value_path: inserting new path");
5520+
e.insert(InferredPath {
5521+
span: *p_span,
5522+
ty: Some(ty_substituted),
5523+
args: None,
5524+
unresolved_vars: vec![],
5525+
});
5526+
}
5527+
Entry::Occupied(mut e) => {
5528+
debug!("instantiate_value_path: updating existing path {:?}", e.get());
5529+
e.get_mut().ty = Some(ty_substituted);
5530+
}
5531+
}
5532+
}
5533+
_ => {}
5534+
}
5535+
}
5536+
54225537
if let Some(UserSelfTy { impl_def_id, self_ty }) = user_self_ty {
54235538
// In the case of `Foo<T>::method` and `<Foo<T>>::method`, if `method`
54245539
// is inherent, there is no `Self` parameter; instead, the impl needs

0 commit comments

Comments
 (0)