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

Also run UnusedBrokenConst on associated consts. #70017

Closed
wants to merge 11 commits into from
11 changes: 9 additions & 2 deletions src/librustc/mir/interpret/queries.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{ConstEvalResult, ErrorHandled, GlobalId};

use crate::mir;
use crate::ty::fold::TypeFoldable;
use crate::ty::subst::{InternalSubsts, SubstsRef};
use crate::ty::{self, TyCtxt};
use rustc_hir::def_id::DefId;
Expand All @@ -9,7 +10,7 @@ use rustc_span::Span;
impl<'tcx> TyCtxt<'tcx> {
/// Evaluates a constant without providing any substitutions. This is useful to evaluate consts
/// that can't take any generic arguments like statics, const items or enum discriminants. If a
/// generic parameter is used within the constant `ErrorHandled::ToGeneric` will be returned.
/// generic parameter is used within the constant `ErrorHandled::TooGeneric` will be returned.
pub fn const_eval_poly(self, def_id: DefId) -> ConstEvalResult<'tcx> {
// In some situations def_id will have substitutions within scope, but they aren't allowed
// to be used. So we can't use `Instance::mono`, instead we feed unresolved substitutions
Expand All @@ -18,7 +19,13 @@ impl<'tcx> TyCtxt<'tcx> {
let substs = InternalSubsts::identity_for_item(self, def_id);
let instance = ty::Instance::new(def_id, substs);
let cid = GlobalId { instance, promoted: None };
let param_env = self.param_env(def_id).with_reveal_all();
let needs_subst = instance.needs_subst();
let param_env = if needs_subst {
self.param_env(def_id)
} else {
self.param_env(def_id).with_reveal_all()
};
debug!("const_eval_poly: needs_subst = {:?}. param_env = {:?}", needs_subst, param_env);
self.const_eval_global_id(param_env, cid, None)
}

Expand Down
16 changes: 11 additions & 5 deletions src/librustc_lint/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use rustc_trait_selection::traits::misc::can_type_implement_copy;

use crate::nonstandard_style::{method_context, MethodLateContext};

use log::debug;
use log::{debug, trace};
use std::fmt::Write;

// hardwired lints from librustc
Expand Down Expand Up @@ -1181,16 +1181,22 @@ fn check_const(cx: &LateContext<'_, '_>, body_id: hir::BodyId) {

impl<'a, 'tcx> LateLintPass<'a, 'tcx> for UnusedBrokenConst {
fn check_item(&mut self, cx: &LateContext<'_, '_>, it: &hir::Item<'_>) {
trace!("UnusedBrokenConst: check_item(_, _, {:?}", it);
match it.kind {
hir::ItemKind::Const(_, body_id) => {
check_const(cx, body_id);
}
hir::ItemKind::Static(_, _, body_id) => {
hir::ItemKind::Const(_, body_id) | hir::ItemKind::Static(_, _, body_id) => {
trace!("UnusedBrokenConst: checking {:?}", body_id);
check_const(cx, body_id);
}
_ => {}
}
}
fn check_impl_item(&mut self, cx: &LateContext<'_, '_>, it: &hir::ImplItem<'_>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can also have associated constants with default values in trait definitions. Also consider const generics here, e.g. foo::<{BAD_CONSTANT}>();.

Copy link
Contributor

Choose a reason for hiding this comment

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

in order to do this you can eval all anonconsts, which will also cover a few other cases (though I think they are already evaluated elsewhere)

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's do this in a separate PR though

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing

trace!("UnusedBrokenConst: check_impl_item(_, _, it = {:?})", it);
if let hir::ImplItemKind::Const(_, body_id) = it.kind {
trace!("UnusedBrokenConst: checking {:?}", body_id);
check_const(cx, body_id);
}
}
}

declare_lint! {
Expand Down
16 changes: 14 additions & 2 deletions src/librustc_mir/const_eval/eval_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ fn validate_and_turn_into_const<'tcx>(
let val = (|| {
let mplace = ecx.raw_const_to_mplace(constant)?;

debug!("validate_and_turn_into_const: mplace = {:?}", mplace);

// FIXME do not validate promoteds until a decision on
// https://github.com/rust-lang/rust/issues/67465 is made
if cid.promoted.is_none() {
Expand Down Expand Up @@ -209,6 +211,7 @@ fn validate_and_turn_into_const<'tcx>(
})();

val.map_err(|error| {
trace!("Validation failed: {:?}", error);
let err = error_to_const_error(&ecx, error);
match err.struct_error(ecx.tcx, "it is undefined behavior to use this value", |mut diag| {
diag.note(note_on_undefined_behavior_error());
Expand Down Expand Up @@ -251,7 +254,14 @@ pub fn const_eval_validated_provider<'tcx>(
});
}

tcx.const_eval_raw(key).and_then(|val| validate_and_turn_into_const(tcx, val, key))
tcx.const_eval_raw(key).and_then(|val| {
trace!(
"const_eval_raw succeeded. val.alloc_id = {:?}. val.ty = {:?}",
val.alloc_id,
val.ty
);
validate_and_turn_into_const(tcx, val, key)
})
}

pub fn const_eval_raw_provider<'tcx>(
Expand All @@ -271,7 +281,9 @@ pub fn const_eval_raw_provider<'tcx>(
key.param_env.reveal = Reveal::UserFacing;
match tcx.const_eval_raw(key) {
// try again with reveal all as requested
Err(ErrorHandled::TooGeneric) => {}
Err(ErrorHandled::TooGeneric) => {
trace!("const_eval_raw: retrying with RevealAll");
}
// deduplicate calls
other => return other,
}
Expand Down
8 changes: 5 additions & 3 deletions src/librustc_mir/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,11 @@ impl<'mir, 'tcx, M: Machine<'mir, 'tcx>> LayoutOf for InterpCx<'mir, 'tcx, M> {

#[inline]
fn layout_of(&self, ty: Ty<'tcx>) -> Self::TyLayout {
self.tcx
.layout_of(self.param_env.and(ty))
.map_err(|layout| err_inval!(Layout(layout)).into())
self.tcx.layout_of(self.param_env.and(ty)).map_err(|layout| {
let result = err_inval!(Layout(layout)).into();
trace!("layout_of: returning error: {:?}", result);
result
})
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/librustc_mir/interpret/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ where
) -> InterpResult<'tcx, MPlaceTy<'tcx, M::PointerTag>> {
let pointee_type =
val.layout.ty.builtin_deref(true).expect("`ref_to_mplace` called on non-ptr type").ty;
let layout = self.layout_of(pointee_type)?;
let layout = self.layout_of(pointee_type);
debug!("ref_to_mplace: layout_of(pointee_type = {:?}) = {:?}", pointee_type, layout);
let layout = layout?;
let (ptr, meta) = match *val {
Immediate::Scalar(ptr) => (ptr.not_undef()?, MemPlaceMeta::None),
Immediate::ScalarPair(ptr, meta) => {
Expand Down Expand Up @@ -1134,6 +1136,7 @@ where
) -> InterpResult<'tcx, MPlaceTy<'tcx, M::PointerTag>> {
// This must be an allocation in `tcx`
assert!(self.tcx.alloc_map.lock().get(raw.alloc_id).is_some());
trace!("raw_const_to_mplace: raw.ty = {:?}", raw.ty);
let ptr = self.tag_global_base_pointer(Pointer::from(raw.alloc_id));
let layout = self.layout_of(raw.ty)?;
Ok(MPlaceTy::from_aligned_ptr(ptr, layout))
Expand Down
28 changes: 19 additions & 9 deletions src/librustc_mir/interpret/validity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,15 +475,25 @@ impl<'rt, 'mir, 'tcx, M: Machine<'mir, 'tcx>> ValidityVisitor<'rt, 'mir, 'tcx, M
Ok(true)
}
ty::RawPtr(..) => {
// We are conservative with undef for integers, but try to
// actually enforce our current rules for raw pointers.
let place = try_validation!(
self.ecx.ref_to_mplace(self.ecx.read_immediate(value)?),
"undefined pointer",
self.path
);
if place.layout.is_unsized() {
self.check_wide_ptr_meta(place.meta, place.layout)?;
let imm = self.ecx.read_immediate(value)?;
let pointee_kind = &imm.layout.ty.builtin_deref(true).unwrap().ty.kind;
match pointee_kind {
ty::Param(_) => {
// Creating pointers to T is valid. We only reach this case for unused
// associated consts.
Comment on lines +482 to +483
Copy link
Member

Choose a reason for hiding this comment

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

This change only affects raw pointers. And it anyway was only checking that the pointer itself is not Undef -- whatever the pointer points to is already irrelevant here. I don't see how this change makes any sense.

However, validity certainly assumes that the type is fully known. It makes little sense to call validity at all when there are still ty::Param around.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I still have ty::Param around in cases for unused associated consts, unfortunately, because I don't have a use-site to pull a type from. When ref_to_mplace calls layout_of, because the type is unknown, the layout is unknown, and validation fails. This breaks code which like this:

impl<T> Nullable for *const T {
const NULL: Self = core::ptr::null::<T>();

I didn't realise that this was just checking for Undef of the pointer value itself. I got the impression the purpose of this check was to check that the pointee had a valid layout (ie, that it's okay to dereference the raw pointer), so I thought skipping this check made sense because it's not until the pointer is dereferenced that any issues could occur.

I see a lot of const errors are picked up during const_eval_raw, before validation even happens - does it make sense to just not run validation for unused associated consts, then?

Copy link
Member

Choose a reason for hiding this comment

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

ie, that it's okay to dereference the raw pointer

Raw pointers may dangle. So such a check would be incorrect.

I still have ty::Param around in cases for unused associated consts, unfortunately, because I don't have a use-site to pull a type from.

Such constant should not have validation run on them, anyway, We cannot validate them, after all, with some type information missing.

I get the feeling you are sticking values into compiler passes that are not prepared to handle those values. I don't think this approach can work. In other words, I think this (running const-eval on associated consts) is the wrong approach to fix #69021.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this (running const-eval on associated consts) is the wrong approach to fix #69021

After the poking I've done here, I've come to realise the issue is a bit broader than just missing bitshifts. We only get lints for associated consts if const_eval reaches the const through some use of it. That is, all lints for associated consts are missed if the associated const is unused. For example, the out-of-range access here isn't picked up if S::N is left unused:

trait Foo {
    const N: i32;
}

struct S;
impl Foo for S {
    const N: i32 = [10][1];
}

fn main() {
    //println!("{}", S::N);
}

As for running const-eval being the wrong approach, I'd argue that associated consts are suitable for const eval in much of the same way normal consts are: by definition, they, like normal consts, must be able to be evaluated at compile time, and they also can be complex enough that just linting the AST without doing at least some form of const evaluation isn't enough.

I get the feeling you are sticking values into compiler passes that are not prepared to handle those values.

Mmm, I agree with your assessment. However, for the reason above, I'm inclined towards making const-eval work (but perhaps shoehorning const_eval_poly into this is the wrong approach).

Copy link
Member

@RalfJung RalfJung Mar 29, 2020

Choose a reason for hiding this comment

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

As for running const-eval being the wrong approach, I'd argue that associated consts are suitable for const eval in much of the same way normal consts are: by definition, they, like normal consts, must be able to be evaluated at compile time, and they also can be complex enough that just linting the AST without doing at least some form of const evaluation isn't enough.

This only makes sense for monomorphized associated consts. It makes no sense when there are still unknown generics around, like what you are seeing here.

Associated consts with missing generics are much more like const functions than consts. And for const functions, we don't run const-eval either. And indeed the entire concept of executing such a constant/function is meaningless as some inputs are missing!

const fn do_something(b: bool, x: i32) -> i32 {
  if b { x } else { x << 44 }
}

What does it mean do "run" do_something? It means nothing. You can only run do_something after picking some b and x.


I really wonder why you are so focused on const_eval. Most of these lints don't come from const_eval. They come from const_prop. That's how the lint is triggered in run-time (non-const) functions, such as this.

Here is the code triggering the lint:

// Check for exceeding shifts *even if* we cannot evaluate the LHS.
if op == BinOp::Shr || op == BinOp::Shl {
// We need the type of the LHS. We cannot use `place_layout` as that is the type
// of the result, which for checked binops is not the same!
let left_ty = left.ty(&self.local_decls, self.tcx);
let left_size_bits = self.ecx.layout_of(left_ty).ok()?.size.bits();
let right_size = r.layout.size;
let r_bits = r.to_scalar().ok();
// This is basically `force_bits`.
let r_bits = r_bits.and_then(|r| r.to_bits_or_ptr(right_size, &self.tcx).ok());
if r_bits.map_or(false, |b| b >= left_size_bits as u128) {
self.report_assert_as_lint(
lint::builtin::ARITHMETIC_OVERFLOW,
source_info,
"this arithmetic operation will overflow",
AssertKind::Overflow(op),
)?;
}
}

const_prop is an optimization pass that expects to run on polymorphic functions with partial information, unlike const_eval which expects to run on fully monomorphic code. Thus I think the right fix here is to run const_prop on associated consts. That is also why in this comment I talked about the relevant parts of const_prop. I wonder if there was any specific reason you went for const_eval?

Copy link
Contributor

Choose a reason for hiding this comment

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

as some inputs are missing!

I actually love that

impl<T> Nullable for *const T { 
     const NULL: Self = core::ptr::null::<T>(); 

is evaluable polymorphically. Not sure if this is relevant for @davidtwco.

If a constant (associated or not) can be evaluated polymorphically, then we don't need to run it again. We may experiment with this in const_eval_raw.

back to topic:

I think we should do something where associated constants and regular constants get handled the same way by UnusedBrokenConst. Not sure what that is, but the current PR is heading in that direction although maybe not on the path that we want. The reason we even have UnusedBrokenConst instead of doing everything in const_prop is the statement at the entry function of const_prop: we're going to evaluate the constant anyway.

So... I'm not sure what I think is the best way forward, but in general doing const_prop is more powerful because it doesn't bail out early if there's an error.

Copy link
Member

@RalfJung RalfJung Mar 29, 2020

Choose a reason for hiding this comment

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

I feel associated consts are more like (const) fn: there might be inputs missing. Thus we need const_prop, which is prepared for that case.

Of course, the actual "propagation" part of const_prop makes little sense here. Maybe putting lint and transformation together was just a bad call? (Another weird effect of this is that some warnings only appear in release mode, such as this one.) (This is already fixed on beta.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wonder if there was any specific reason you went for const_eval?

I looked into how unused non-assoc constants were linted and found UnusedBrokenConst, which runs const_eval_poly. I was hoping running const_eval_poly would handle all the cases where there weren't missing substitutions, and just bail out with TooGeneric in other cases:

/// Evaluates a constant without providing any substitutions. This is useful to evaluate consts
/// that can't take any generic arguments like statics, const items or enum discriminants. If a
/// generic parameter is used within the constant `ErrorHandled::ToGeneric` will be returned.
pub fn const_eval_poly(self, def_id: DefId) -> ConstEvalResult<'tcx> {

On a second reading, and in light of everything you've both informed me here, I've realised I've focussed on the wrong part of that doc comment.

Associated consts with missing generics are much more like const functions than consts. And for const functions, we don't run const-eval either. And indeed the entire concept of executing such a constant/function is meaningless as some inputs are missing!

const_prop is an optimization pass that expects to run on polymorphic functions with partial information, unlike const_eval which expects to run on fully monomorphic code.

in general doing const_prop is more powerful because it doesn't bail out early if there's an error

Ah, and that's why const_eval_poly was suitable for normal consts, but is not suitable for associated consts.

Maybe putting lint and transformation together was just a bad call

I'm inclined to agree, and moreso when the transformation isn't always run. I'll pull the linting code into its own function as a very first step.

}
_ => {
// We are conservative with undef for integers, but try to
// actually enforce our current rules for raw pointers.
let place = try_validation!(
self.ecx.ref_to_mplace(imm),
"undefined pointer",
self.path
);
if place.layout.is_unsized() {
self.check_wide_ptr_meta(place.meta, place.layout)?;
}
}
}
Ok(true)
}
Expand Down
29 changes: 29 additions & 0 deletions src/test/ui/associated-const/lints-used-unused.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Tests that associated constants are checked whether they are used or not.
//
// revisions: used unused
// compile-flags: -Copt-level=2 --emit link

#![cfg_attr(unused, allow(dead_code))]
#![deny(arithmetic_overflow)]

pub trait Foo {
const N: i32;
}

struct S;

impl Foo for S {
const N: i32 = 1 << 42;
//~^ ERROR this arithmetic operation will overflow
//~| ERROR any use of this value will cause an error
}

impl<T: Foo> Foo for Vec<T> {
const N: i32 = --T::N + (-i32::MIN);
//~^ ERROR this arithmetic operation will overflow
}

fn main() {
#[cfg(used)]
let _ = S::N; // FIXME: "erroneous constant used" -- awaiting #67176
}
30 changes: 30 additions & 0 deletions src/test/ui/associated-const/lints-used-unused.unused.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
error: this arithmetic operation will overflow
--> $DIR/lints-used-unused.rs:16:20
|
LL | const N: i32 = 1 << 42;
| ^^^^^^^ attempt to shift left with overflow
|
note: the lint level is defined here
--> $DIR/lints-used-unused.rs:7:9
|
LL | #![deny(arithmetic_overflow)]
| ^^^^^^^^^^^^^^^^^^^

error: any use of this value will cause an error
--> $DIR/lints-used-unused.rs:16:20
|
LL | const N: i32 = 1 << 42;
| ---------------^^^^^^^-
| |
| attempt to shift left with overflow
|
= note: `#[deny(const_err)]` on by default

error: this arithmetic operation will overflow
--> $DIR/lints-used-unused.rs:22:29
|
LL | const N: i32 = --T::N + (-i32::MIN);
| ^^^^^^^^^^^ attempt to negate with overflow

error: aborting due to 3 previous errors

30 changes: 30 additions & 0 deletions src/test/ui/associated-const/lints-used-unused.used.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
error: this arithmetic operation will overflow
--> $DIR/lints-used-unused.rs:16:20
|
LL | const N: i32 = 1 << 42;
| ^^^^^^^ attempt to shift left with overflow
|
note: the lint level is defined here
--> $DIR/lints-used-unused.rs:7:9
|
LL | #![deny(arithmetic_overflow)]
| ^^^^^^^^^^^^^^^^^^^

error: any use of this value will cause an error
--> $DIR/lints-used-unused.rs:16:20
|
LL | const N: i32 = 1 << 42;
| ---------------^^^^^^^-
| |
| attempt to shift left with overflow
|
= note: `#[deny(const_err)]` on by default
Comment on lines +1 to +21
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does it make sense to try to deduplicate these errors? From a front-facing perspective, they're two different lints (arithmetic_overflow and const_err), and if someone wanted to silence these errors, they would have to disable both lints. What's the go for duplicates like this?


error: this arithmetic operation will overflow
--> $DIR/lints-used-unused.rs:22:29
|
LL | const N: i32 = --T::N + (-i32::MIN);
| ^^^^^^^^^^^ attempt to negate with overflow

error: aborting due to 3 previous errors

42 changes: 38 additions & 4 deletions src/test/ui/consts/issue-69020.noopt.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,59 @@ LL | const NEG: i32 = -i32::MIN + T::NEG;
|
= note: `#[deny(arithmetic_overflow)]` on by default

error: any use of this value will cause an error
--> $DIR/issue-69020.rs:21:22
|
LL | const NEG: i32 = -i32::MIN + T::NEG;
| -----------------^^^^^^^^^----------
| |
| attempt to negate with overflow
|
= note: `#[deny(const_err)]` on by default

error: this arithmetic operation will overflow
--> $DIR/issue-69020.rs:23:22
--> $DIR/issue-69020.rs:24:22
|
LL | const ADD: i32 = (i32::MAX+1) + T::ADD;
| ^^^^^^^^^^^^ attempt to add with overflow

error: any use of this value will cause an error
--> $DIR/issue-69020.rs:24:22
|
LL | const ADD: i32 = (i32::MAX+1) + T::ADD;
| -----------------^^^^^^^^^^^^----------
| |
| attempt to add with overflow

error: this operation will panic at runtime
--> $DIR/issue-69020.rs:25:22
--> $DIR/issue-69020.rs:27:22
|
LL | const DIV: i32 = (1/0) + T::DIV;
| ^^^^^ attempt to divide by zero
|
= note: `#[deny(unconditional_panic)]` on by default

error: this operation will panic at runtime
error: any use of this value will cause an error
--> $DIR/issue-69020.rs:27:22
|
LL | const DIV: i32 = (1/0) + T::DIV;
| -----------------^^^^^----------
| |
| attempt to divide by zero

error: this operation will panic at runtime
--> $DIR/issue-69020.rs:30:22
|
LL | const OOB: i32 = [1][1] + T::OOB;
| ^^^^^^ index out of bounds: the len is 1 but the index is 1

error: aborting due to 4 previous errors
error: any use of this value will cause an error
--> $DIR/issue-69020.rs:30:22
|
LL | const OOB: i32 = [1][1] + T::OOB;
| -----------------^^^^^^----------
| |
| index out of bounds: the len is 1 but the index is 1

error: aborting due to 8 previous errors

Loading