Skip to content

Commit 7183fa0

Browse files
committed
promotion: do not promote const-fn calls in const when that may fail without the entire const failing
1 parent 40dcd79 commit 7183fa0

11 files changed

+196
-250
lines changed

compiler/rustc_mir_transform/src/lib.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -333,13 +333,6 @@ fn mir_promoted(
333333
body.tainted_by_errors = Some(error_reported);
334334
}
335335

336-
let mut required_consts = Vec::new();
337-
let mut required_consts_visitor = RequiredConstsVisitor::new(&mut required_consts);
338-
for (bb, bb_data) in traversal::reverse_postorder(&body) {
339-
required_consts_visitor.visit_basic_block_data(bb, bb_data);
340-
}
341-
body.required_consts = required_consts;
342-
343336
// What we need to run borrowck etc.
344337
let promote_pass = promote_consts::PromoteTemps::default();
345338
pm::run_passes(
@@ -349,6 +342,14 @@ fn mir_promoted(
349342
Some(MirPhase::Analysis(AnalysisPhase::Initial)),
350343
);
351344

345+
// Promotion generates new consts; we run this after promotion to ensure they are accounted for.
346+
let mut required_consts = Vec::new();
347+
let mut required_consts_visitor = RequiredConstsVisitor::new(&mut required_consts);
348+
for (bb, bb_data) in traversal::reverse_postorder(&body) {
349+
required_consts_visitor.visit_basic_block_data(bb, bb_data);
350+
}
351+
body.required_consts = required_consts;
352+
352353
let promoted = promote_pass.promoted_fragments.into_inner();
353354
(tcx.alloc_steal_mir(body), tcx.alloc_steal_promoted(promoted))
354355
}

compiler/rustc_mir_transform/src/promote_consts.rs

+79-19
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! move analysis runs after promotion on broken MIR.
1414
1515
use either::{Left, Right};
16+
use rustc_data_structures::fx::FxHashSet;
1617
use rustc_hir as hir;
1718
use rustc_middle::mir;
1819
use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor};
@@ -175,6 +176,12 @@ fn collect_temps_and_candidates<'tcx>(
175176
struct Validator<'a, 'tcx> {
176177
ccx: &'a ConstCx<'a, 'tcx>,
177178
temps: &'a mut IndexSlice<Local, TempState>,
179+
/// For backwards compatibility, we are promoting function calls in `const`/`static`
180+
/// initializers. But we want to avoid evaluating code that might panic and that otherwise would
181+
/// not have been evaluated, so we only promote such calls in basic blocks that are guaranteed
182+
/// to execute. In other words, we only promote such calls in basic blocks that are definitely
183+
/// not dead code. Here we cache the result of computing that set of basic blocks.
184+
promotion_safe_blocks: Option<FxHashSet<BasicBlock>>,
178185
}
179186

180187
impl<'a, 'tcx> std::ops::Deref for Validator<'a, 'tcx> {
@@ -260,7 +267,9 @@ impl<'tcx> Validator<'_, 'tcx> {
260267
self.validate_rvalue(rhs)
261268
}
262269
Right(terminator) => match &terminator.kind {
263-
TerminatorKind::Call { func, args, .. } => self.validate_call(func, args),
270+
TerminatorKind::Call { func, args, .. } => {
271+
self.validate_call(func, args, loc.block)
272+
}
264273
TerminatorKind::Yield { .. } => Err(Unpromotable),
265274
kind => {
266275
span_bug!(terminator.source_info.span, "{:?} not promotable", kind);
@@ -588,42 +597,93 @@ impl<'tcx> Validator<'_, 'tcx> {
588597
Ok(())
589598
}
590599

600+
/// Computes the sets of blocks of this MIR that are definitely going to be executed
601+
/// if the function returns successfully. That makes it safe to promote calls in them
602+
/// that might fail.
603+
fn promotion_safe_blocks(body: &mir::Body<'tcx>) -> FxHashSet<BasicBlock> {
604+
let mut safe_blocks = FxHashSet::default();
605+
let mut safe_block = START_BLOCK;
606+
loop {
607+
safe_blocks.insert(safe_block);
608+
// Let's see if we can find another safe block.
609+
safe_block = match body.basic_blocks[safe_block].terminator().kind {
610+
TerminatorKind::Goto { target } => target,
611+
TerminatorKind::Call { target: Some(target), .. }
612+
| TerminatorKind::Drop { target, .. } => {
613+
// This calls a function or the destructor. `target` does not get executed if
614+
// the callee loops or panics. But in both cases the const already fails to
615+
// evaluate, so we are fine considering `target` a safe block for promotion.
616+
target
617+
}
618+
TerminatorKind::Assert { target, .. } => {
619+
// Similar to above, we only consider successful execution.
620+
target
621+
}
622+
_ => {
623+
// No next safe block.
624+
break;
625+
}
626+
};
627+
}
628+
safe_blocks
629+
}
630+
631+
/// Returns whether the block is "safe" for promotion, which means it cannot be dead code.
632+
/// We use this to avoid promoting operations that can fail in dead code.
633+
fn is_promotion_safe_block(&mut self, block: BasicBlock) -> bool {
634+
let body = self.body;
635+
let safe_blocks =
636+
self.promotion_safe_blocks.get_or_insert_with(|| Self::promotion_safe_blocks(body));
637+
safe_blocks.contains(&block)
638+
}
639+
591640
fn validate_call(
592641
&mut self,
593642
callee: &Operand<'tcx>,
594643
args: &[Spanned<Operand<'tcx>>],
644+
block: BasicBlock,
595645
) -> Result<(), Unpromotable> {
646+
// Validate the operands. If they fail, there's no question -- we cannot promote.
647+
self.validate_operand(callee)?;
648+
for arg in args {
649+
self.validate_operand(&arg.node)?;
650+
}
651+
652+
// Functions marked `#[rustc_promotable]` are explicitly allowed to be promoted, so we can
653+
// accept them at this point.
596654
let fn_ty = callee.ty(self.body, self.tcx);
655+
if let ty::FnDef(def_id, _) = *fn_ty.kind() {
656+
if self.tcx.is_promotable_const_fn(def_id) {
657+
return Ok(());
658+
}
659+
}
597660

598-
// Inside const/static items, we promote all (eligible) function calls.
599-
// Everywhere else, we require `#[rustc_promotable]` on the callee.
600-
let promote_all_const_fn = matches!(
661+
// Ideally, we'd stop here and reject the rest.
662+
// But for backward compatibility, we have to accept some promotion in const/static
663+
// initializers. Inline consts are explicitly excluded, they are more recent so we have no
664+
// backwards compatibility reason to allow more promotion inside of them.
665+
let promote_all_fn = matches!(
601666
self.const_kind,
602667
Some(hir::ConstContext::Static(_) | hir::ConstContext::Const { inline: false })
603668
);
604-
if !promote_all_const_fn {
605-
if let ty::FnDef(def_id, _) = *fn_ty.kind() {
606-
// Never promote runtime `const fn` calls of
607-
// functions without `#[rustc_promotable]`.
608-
if !self.tcx.is_promotable_const_fn(def_id) {
609-
return Err(Unpromotable);
610-
}
611-
}
669+
if !promote_all_fn {
670+
return Err(Unpromotable);
612671
}
613-
672+
// Make sure the callee is a `const fn`.
614673
let is_const_fn = match *fn_ty.kind() {
615674
ty::FnDef(def_id, _) => self.tcx.is_const_fn_raw(def_id),
616675
_ => false,
617676
};
618677
if !is_const_fn {
619678
return Err(Unpromotable);
620679
}
621-
622-
self.validate_operand(callee)?;
623-
for arg in args {
624-
self.validate_operand(&arg.node)?;
680+
// The problem is, this may promote calls to functions that panic.
681+
// We don't want to introduce compilation errors if there's a panic in a call in dead code.
682+
// So we ensure that this is not dead code.
683+
if !self.is_promotion_safe_block(block) {
684+
return Err(Unpromotable);
625685
}
626-
686+
// This passed all checks, so let's accept.
627687
Ok(())
628688
}
629689
}
@@ -634,7 +694,7 @@ fn validate_candidates(
634694
temps: &mut IndexSlice<Local, TempState>,
635695
candidates: &[Candidate],
636696
) -> Vec<Candidate> {
637-
let mut validator = Validator { ccx, temps };
697+
let mut validator = Validator { ccx, temps, promotion_safe_blocks: None };
638698

639699
candidates
640700
.iter()

tests/ui/consts/const-eval/promoted_errors.noopt.stderr

-44
This file was deleted.

tests/ui/consts/const-eval/promoted_errors.opt.stderr

-44
This file was deleted.

tests/ui/consts/const-eval/promoted_errors.opt_with_overflow_checks.stderr

-44
This file was deleted.

tests/ui/consts/const-eval/promoted_errors.rs

-52
This file was deleted.

tests/ui/consts/promote-not.rs

+9
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ const TEST_DROP_NOT_PROMOTE: &String = {
5151
};
5252

5353

54+
// We do not promote function calls in `const` initializers in dead code.
55+
const fn mk_panic() -> u32 { panic!() }
56+
const fn mk_false() -> bool { false }
57+
const Y: () = {
58+
if mk_false() {
59+
let _x: &'static u32 = &mk_panic(); //~ ERROR temporary value dropped while borrowed
60+
}
61+
};
62+
5463
fn main() {
5564
// We must not promote things with interior mutability. Not even if we "project it away".
5665
let _val: &'static _ = &(Cell::new(1), 2).0; //~ ERROR temporary value dropped while borrowed

0 commit comments

Comments
 (0)