Skip to content

Commit

Permalink
Auto merge of #103877 - oli-obk:const_eval_step_limit, r=fee1-dead
Browse files Browse the repository at this point in the history
Replace const eval limit by a lint and add an exponential backoff warning

The lint triggers at the first power of 2 that comes after 1 million function calls or traversed back-edges (takes less than a second on usual programs). After the first emission, an unsilenceable warning is repeated at every following power of 2 terminators, causing it to get reported less and less the longer the evaluation runs.

cc `@rust-lang/wg-const-eval`

fixes #93481
closes #67217
  • Loading branch information
bors committed Jun 1, 2023
2 parents ba1690b + 05eae08 commit 23f93a1
Show file tree
Hide file tree
Showing 41 changed files with 325 additions and 261 deletions.
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();
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 {
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))
}

#[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

0 comments on commit 23f93a1

Please sign in to comment.