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

Replace const eval limit by a lint and add an exponential backoff warning #103877

Merged
merged 1 commit into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions compiler/rustc_const_eval/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ const_eval_interior_mutable_data_refer =
This would make multiple uses of a constant to be able to see different values and allow circumventing
the `Send` and `Sync` requirements for shared mutable data, which is unsound.
const_eval_long_running =
constant evaluation is taking a long time
.note = this lint makes sure the compiler doesn't get stuck due to infinite loops in const eval.
If your compilation actually takes a long time, you can safely allow the lint.
.label = the const evaluator is currently interpreting this expression
.help = the constant being evaluated
const_eval_max_num_nodes_in_const = maximum number of nodes exceeded in constant {$global_const_id}
const_eval_mut_deref =
Expand Down
3 changes: 1 addition & 2 deletions compiler/rustc_const_eval/src/const_eval/eval_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ pub(super) fn mk_eval_cx<'mir, 'tcx>(
tcx,
root_span,
param_env,
CompileTimeInterpreter::new(tcx.const_eval_limit(), can_access_statics, CheckAlignment::No),
CompileTimeInterpreter::new(can_access_statics, CheckAlignment::No),
)
}

Expand Down Expand Up @@ -306,7 +306,6 @@ pub fn eval_to_allocation_raw_provider<'tcx>(
// Statics (and promoteds inside statics) may access other statics, because unlike consts
// they do not have to behave "as if" they were evaluated at runtime.
CompileTimeInterpreter::new(
tcx.const_eval_limit(),
/*can_access_statics:*/ is_static,
if tcx.sess.opts.unstable_opts.extra_const_ub_checks {
CheckAlignment::Error
Expand Down
82 changes: 65 additions & 17 deletions compiler/rustc_const_eval/src/const_eval/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,36 @@ use std::fmt;
use rustc_ast::Mutability;
use rustc_hir::def_id::DefId;
use rustc_middle::mir::AssertMessage;
use rustc_session::Limit;
use rustc_span::symbol::{sym, Symbol};
use rustc_target::abi::{Align, Size};
use rustc_target::spec::abi::Abi as CallAbi;

use crate::errors::{LongRunning, LongRunningWarn};
use crate::interpret::{
self, compile_time_machine, AllocId, ConstAllocation, FnVal, Frame, ImmTy, InterpCx,
InterpResult, OpTy, PlaceTy, Pointer, Scalar,
};

use super::error::*;

/// When hitting this many interpreted terminators we emit a deny by default lint
/// that notfies the user that their constant takes a long time to evaluate. If that's
/// what they intended, they can just allow the lint.
const LINT_TERMINATOR_LIMIT: usize = 2_000_000;
/// The limit used by `-Z tiny-const-eval-limit`. This smaller limit is useful for internal
/// tests not needing to run 30s or more to show some behaviour.
const TINY_LINT_TERMINATOR_LIMIT: usize = 20;
/// After this many interpreted terminators, we start emitting progress indicators at every
/// power of two of interpreted terminators.
const PROGRESS_INDICATOR_START: usize = 4_000_000;

/// Extra machine state for CTFE, and the Machine instance
pub struct CompileTimeInterpreter<'mir, 'tcx> {
/// For now, the number of terminators that can be evaluated before we throw a resource
/// exhaustion error.
/// The number of terminators that have been evaluated.
///
/// Setting this to `0` disables the limit and allows the interpreter to run forever.
pub(super) steps_remaining: usize,
/// This is used to produce lints informing the user that the compiler is not stuck.
/// Set to `usize::MAX` to never report anything.
pub(super) num_evaluated_steps: usize,

/// The virtual call stack.
pub(super) stack: Vec<Frame<'mir, 'tcx, AllocId, ()>>,
Expand Down Expand Up @@ -72,13 +83,9 @@ impl CheckAlignment {
}

impl<'mir, 'tcx> CompileTimeInterpreter<'mir, 'tcx> {
pub(crate) fn new(
const_eval_limit: Limit,
can_access_statics: bool,
check_alignment: CheckAlignment,
) -> Self {
pub(crate) fn new(can_access_statics: bool, check_alignment: CheckAlignment) -> Self {
CompileTimeInterpreter {
steps_remaining: const_eval_limit.0,
num_evaluated_steps: 0,
stack: Vec::new(),
can_access_statics,
check_alignment,
Expand Down Expand Up @@ -569,13 +576,54 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for CompileTimeInterpreter<'mir,

fn increment_const_eval_counter(ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
// The step limit has already been hit in a previous call to `increment_const_eval_counter`.
if ecx.machine.steps_remaining == 0 {
return Ok(());
}

ecx.machine.steps_remaining -= 1;
if ecx.machine.steps_remaining == 0 {
throw_exhaust!(StepLimitReached)
if let Some(new_steps) = ecx.machine.num_evaluated_steps.checked_add(1) {
let (limit, start) = if ecx.tcx.sess.opts.unstable_opts.tiny_const_eval_limit {
(TINY_LINT_TERMINATOR_LIMIT, TINY_LINT_TERMINATOR_LIMIT)
} else {
(LINT_TERMINATOR_LIMIT, PROGRESS_INDICATOR_START)
};

ecx.machine.num_evaluated_steps = new_steps;
// By default, we have a *deny* lint kicking in after some time
// to ensure `loop {}` doesn't just go forever.
// In case that lint got reduced, in particular for `--cap-lint` situations, we also
// have a hard warning shown every now and then for really long executions.
if new_steps == limit {
// By default, we stop after a million steps, but the user can disable this lint
// to be able to run until the heat death of the universe or power loss, whichever
// comes first.
let hir_id = ecx.best_lint_scope();
let is_error = ecx
.tcx
.lint_level_at_node(
rustc_session::lint::builtin::LONG_RUNNING_CONST_EVAL,
hir_id,
)
.0
.is_error();
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
let span = ecx.cur_span();
ecx.tcx.emit_spanned_lint(
rustc_session::lint::builtin::LONG_RUNNING_CONST_EVAL,
hir_id,
span,
LongRunning { item_span: ecx.tcx.span },
);
// If this was a hard error, don't bother continuing evaluation.
if is_error {
oli-obk marked this conversation as resolved.
Show resolved Hide resolved
let guard = ecx
.tcx
.sess
.delay_span_bug(span, "The deny lint should have already errored");
throw_inval!(AlreadyReported(guard.into()));
}
} else if new_steps > start && new_steps.is_power_of_two() {
// Only report after a certain number of terminators have been evaluated and the
// current number of evaluated terminators is a power of 2. The latter gives us a cheap
// way to implement exponential backoff.
let span = ecx.cur_span();
ecx.tcx.sess.emit_warning(LongRunningWarn { span, item_span: ecx.tcx.span });
}
}

Ok(())
Expand Down
20 changes: 19 additions & 1 deletion compiler/rustc_const_eval/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use rustc_hir::ConstContext;
use rustc_macros::Diagnostic;
use rustc_macros::{Diagnostic, LintDiagnostic};
use rustc_span::Span;

#[derive(Diagnostic)]
Expand Down Expand Up @@ -194,3 +194,21 @@ pub(crate) struct InteriorMutabilityBorrow {
#[primary_span]
pub span: Span,
}

#[derive(LintDiagnostic)]
#[diag(const_eval_long_running)]
#[note]
pub struct LongRunning {
#[help]
pub item_span: Span,
}

#[derive(Diagnostic)]
#[diag(const_eval_long_running)]
pub struct LongRunningWarn {
#[primary_span]
#[label]
pub span: Span,
#[help]
pub item_span: Span,
}
10 changes: 10 additions & 0 deletions compiler/rustc_const_eval/src/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::mem;

use either::{Either, Left, Right};

use hir::CRATE_HIR_ID;
use rustc_hir::{self as hir, def_id::DefId, definitions::DefPathData};
use rustc_index::IndexVec;
use rustc_middle::mir;
Expand Down Expand Up @@ -405,6 +406,15 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
self.stack().last().map_or(self.tcx.span, |f| f.current_span())
}

#[inline(always)]
/// Find the first stack frame that is within the current crate, if any, otherwise return the crate's HirId
pub fn best_lint_scope(&self) -> hir::HirId {
self.stack()
.iter()
.find_map(|frame| frame.body.source.def_id().as_local())
.map_or(CRATE_HIR_ID, |def_id| self.tcx.hir().local_def_id_to_hir_id(def_id))
}
oli-obk marked this conversation as resolved.
Show resolved Hide resolved

#[inline(always)]
pub(crate) fn stack(&self) -> &[Frame<'mir, 'tcx, M::Provenance, M::FrameExtra>] {
M::stack(self)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use rustc_middle::ty::layout::{LayoutCx, LayoutError, LayoutOf, TyAndLayout, ValidityRequirement};
use rustc_middle::ty::{ParamEnv, ParamEnvAnd, Ty, TyCtxt};
use rustc_session::Limit;
use rustc_target::abi::{Abi, FieldsShape, Scalar, Variants};

use crate::const_eval::{CheckAlignment, CompileTimeInterpreter};
Expand Down Expand Up @@ -45,11 +44,8 @@ fn might_permit_raw_init_strict<'tcx>(
tcx: TyCtxt<'tcx>,
kind: ValidityRequirement,
) -> Result<bool, LayoutError<'tcx>> {
let machine = CompileTimeInterpreter::new(
Limit::new(0),
/*can_access_statics:*/ false,
CheckAlignment::Error,
);
let machine =
CompileTimeInterpreter::new(/*can_access_statics:*/ false, CheckAlignment::Error);

let mut cx = InterpCx::new(tcx, rustc_span::DUMMY_SP, ParamEnv::reveal_all(), machine);

Expand Down
2 changes: 0 additions & 2 deletions compiler/rustc_feature/src/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,6 @@ declare_features! (
(active, const_async_blocks, "1.53.0", Some(85368), None),
/// Allows `const || {}` closures in const contexts.
(incomplete, const_closures, "1.68.0", Some(106003), None),
/// Allows limiting the evaluation steps of const expressions
(active, const_eval_limit, "1.43.0", Some(67217), None),
/// Allows the definition of `const extern fn` and `const unsafe extern fn`.
(active, const_extern_fn, "1.40.0", Some(64926), None),
/// Allows basic arithmetic on floating point types in a `const fn`.
Expand Down
4 changes: 0 additions & 4 deletions compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,6 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
// Limits:
ungated!(recursion_limit, CrateLevel, template!(NameValueStr: "N"), FutureWarnFollowing),
ungated!(type_length_limit, CrateLevel, template!(NameValueStr: "N"), FutureWarnFollowing),
gated!(
const_eval_limit, CrateLevel, template!(NameValueStr: "N"), ErrorFollowing,
const_eval_limit, experimental!(const_eval_limit)
),
gated!(
move_size_limit, CrateLevel, template!(NameValueStr: "N"), ErrorFollowing,
large_assignments, experimental!(move_size_limit)
Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_feature/src/removed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ declare_features! (
/// Allows comparing raw pointers during const eval.
(removed, const_compare_raw_pointers, "1.46.0", Some(53020), None,
Some("cannot be allowed in const eval in any meaningful way")),
/// Allows limiting the evaluation steps of const expressions
(removed, const_eval_limit, "1.43.0", Some(67217), None, Some("removed the limit entirely")),
/// Allows non-trivial generic constants which have to be manually propagated upwards.
(removed, const_evaluatable_checked, "1.48.0", Some(76560), None, Some("renamed to `generic_const_exprs`")),
(removed, const_evaluatable_checked, "1.48.0", Some(76560), None, Some("renamed to `generic_const_exprs`")),
/// Allows the definition of `const` functions with some advanced features.
(removed, const_fn, "1.54.0", Some(57563), None,
Some("split into finer-grained feature gates")),
Expand Down
38 changes: 38 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3357,6 +3357,7 @@ declare_lint_pass! {
LARGE_ASSIGNMENTS,
LATE_BOUND_LIFETIME_ARGUMENTS,
LEGACY_DERIVE_HELPERS,
LONG_RUNNING_CONST_EVAL,
LOSSY_PROVENANCE_CASTS,
MACRO_EXPANDED_MACRO_EXPORTS_ACCESSED_BY_ABSOLUTE_PATHS,
MACRO_USE_EXTERN_CRATE,
Expand Down Expand Up @@ -3426,6 +3427,43 @@ declare_lint_pass! {
]
}

declare_lint! {
/// The `long_running_const_eval` lint is emitted when const
/// eval is running for a long time to ensure rustc terminates
/// even if you accidentally wrote an infinite loop.
///
/// ### Example
///
/// ```rust,compile_fail
/// const FOO: () = loop {};
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Loops allow const evaluation to compute arbitrary code, but may also
/// cause infinite loops or just very long running computations.
/// Users can enable long running computations by allowing the lint
/// on individual constants or for entire crates.
///
/// ### Unconditional warnings
///
/// Note that regardless of whether the lint is allowed or set to warn,
/// the compiler will issue warnings if constant evaluation runs significantly
/// longer than this lint's limit. These warnings are also shown to downstream
/// users from crates.io or similar registries. If you are above the lint's limit,
/// both you and downstream users might be exposed to these warnings.
/// They might also appear on compiler updates, as the compiler makes minor changes
/// about how complexity is measured: staying below the limit ensures that there
/// is enough room, and given that the lint is disabled for people who use your
/// dependency it means you will be the only one to get the warning and can put
/// out an update in your own time.
pub LONG_RUNNING_CONST_EVAL,
Deny,
"detects long const eval operations"
}

declare_lint! {
/// The `unused_doc_comments` lint detects doc comments that aren't used
/// by `rustdoc`.
Expand Down
11 changes: 2 additions & 9 deletions compiler/rustc_middle/src/middle/limits.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Registering limits:
//! * recursion_limit,
//! * move_size_limit,
//! * type_length_limit, and
//! * const_eval_limit
//! * move_size_limit, and
//! * type_length_limit
//!
//! There are various parts of the compiler that must impose arbitrary limits
//! on how deeply they recurse to prevent stack overflow. Users can override
Expand Down Expand Up @@ -34,12 +33,6 @@ pub fn provide(providers: &mut Providers) {
sym::type_length_limit,
1048576,
),
const_eval_limit: get_limit(
tcx.hir().krate_attrs(),
tcx.sess,
sym::const_eval_limit,
2_000_000,
),
}
}

Expand Down
7 changes: 0 additions & 7 deletions compiler/rustc_middle/src/mir/interpret/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,10 +465,6 @@ impl fmt::Display for UnsupportedOpInfo {
pub enum ResourceExhaustionInfo {
/// The stack grew too big.
StackFrameLimitReached,
/// The program ran for too long.
///
/// The exact limit is set by the `const_eval_limit` attribute.
StepLimitReached,
/// There is not enough memory (on the host) to perform an allocation.
MemoryExhausted,
/// The address space (of the target) is full.
Expand All @@ -482,9 +478,6 @@ impl fmt::Display for ResourceExhaustionInfo {
StackFrameLimitReached => {
write!(f, "reached the configured maximum number of stack frames")
}
StepLimitReached => {
write!(f, "exceeded interpreter step limit (see `#[const_eval_limit]`)")
}
MemoryExhausted => {
write!(f, "tried to allocate more memory than available to compiler")
}
Expand Down
10 changes: 0 additions & 10 deletions compiler/rustc_middle/src/ty/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ use std::iter;
use std::mem;
use std::ops::{Bound, Deref};

const TINY_CONST_EVAL_LIMIT: Limit = Limit(20);

#[allow(rustc::usage_of_ty_tykind)]
impl<'tcx> Interner for TyCtxt<'tcx> {
type AdtDef = ty::AdtDef<'tcx>;
Expand Down Expand Up @@ -1178,14 +1176,6 @@ impl<'tcx> TyCtxt<'tcx> {
self.limits(()).move_size_limit
}

pub fn const_eval_limit(self) -> Limit {
if self.sess.opts.unstable_opts.tiny_const_eval_limit {
TINY_CONST_EVAL_LIMIT
} else {
self.limits(()).const_eval_limit
}
}

pub fn all_traits(self) -> impl Iterator<Item = DefId> + 'tcx {
iter::once(LOCAL_CRATE)
.chain(self.crates(()).iter().copied())
Expand Down
2 changes: 0 additions & 2 deletions compiler/rustc_session/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ pub struct Limits {
pub move_size_limit: Limit,
/// The maximum length of types during monomorphization.
pub type_length_limit: Limit,
/// The maximum blocks a const expression can evaluate.
pub const_eval_limit: Limit,
}

pub struct CompilerIO {
Expand Down
2 changes: 1 addition & 1 deletion src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ RUN ./build-clang.sh
ENV CC=clang CXX=clang++

# rustc-perf version from 2023-03-15
ENV PERF_COMMIT 9dfaa35193154b690922347ee1141a06ec87a199
ENV PERF_COMMIT 8b2ac3042e1ff2c0074455a0a3618adef97156b1
RUN curl -LS -o perf.zip https://github.com/rust-lang/rustc-perf/archive/$PERF_COMMIT.zip && \
unzip perf.zip && \
mv rustc-perf-$PERF_COMMIT rustc-perf && \
Expand Down
Loading