Skip to content

Commit 983b9dc

Browse files
committed
Auto merge of rust-lang#121557 - RalfJung:const-fn-call-promotion, r=<try>
restrict promotion of `const fn` calls We only promote them in `const`/`static` initializers, but even that is still unfortunate -- we still cannot add promoteds to required_consts. But we should add them there to make sure it's always okay to evaluate every const we encounter in a MIR body. That effort of not promoting things that can fail to evaluate is tracked in rust-lang#80619. These `const fn` calls are the last missing piece. So I propose that we do not promote const-fn calls in const when that may fail without the entire const failing, thereby completing rust-lang#80619. Unfortunately we can't just reject promoting these functions outright due to backwards compatibility. So let's see if we can find a hack that makes crater happy... For the record, this is the [crater analysis](rust-lang#80243 (comment)) from when I tried to entirely forbid this kind of promotion. It's a tiny amount of breakage and if we had a nice alternative for code like that, we could conceivably push it through... but sadly, inline const expressions are still blocked on t-lang concerns about post-monomorphization errors and we haven't yet figured out an implementation that can resolve those concerns. So we're forced to make progress via other means, such as terrible hacks like this. Attempt one: only promote calls on the "safe path" at the beginning of a MIR block. This is the path that starts at the start block and continues via gotos and calls, but stops at the first branch. If we had imposed this restriction before stabilizing `if` and `match` in `const`, this would have definitely been sufficient... r? `@oli-obk`
2 parents 094a620 + 8f4d144 commit 983b9dc

11 files changed

+190
-246
lines changed

compiler/rustc_mir_transform/src/lib.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,6 @@ fn mir_promoted(
343343
body.tainted_by_errors = Some(error_reported);
344344
}
345345

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-
353346
// What we need to run borrowck etc.
354347
let promote_pass = promote_consts::PromoteTemps::default();
355348
pm::run_passes(
@@ -359,6 +352,14 @@ fn mir_promoted(
359352
Some(MirPhase::Analysis(AnalysisPhase::Initial)),
360353
);
361354

355+
// Promotion generates new consts; we run this after promotion to ensure they are accounted for.
356+
let mut required_consts = Vec::new();
357+
let mut required_consts_visitor = RequiredConstsVisitor::new(&mut required_consts);
358+
for (bb, bb_data) in traversal::reverse_postorder(&body) {
359+
required_consts_visitor.visit_basic_block_data(bb, bb_data);
360+
}
361+
body.required_consts = required_consts;
362+
362363
let promoted = promote_pass.promoted_fragments.into_inner();
363364
(tcx.alloc_steal_mir(body), tcx.alloc_steal_promoted(promoted))
364365
}

compiler/rustc_mir_transform/src/promote_consts.rs

+77-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,11 @@ 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 otherwise would not have been
181+
/// evaluated, so we only do thos in basic blocks that are guaranteed to evaluate. Here we cache
182+
/// the result of computing that set of basic blocks.
183+
promotion_safe_blocks: Option<FxHashSet<BasicBlock>>,
178184
}
179185

180186
impl<'a, 'tcx> std::ops::Deref for Validator<'a, 'tcx> {
@@ -260,7 +266,9 @@ impl<'tcx> Validator<'_, 'tcx> {
260266
self.validate_rvalue(rhs)
261267
}
262268
Right(terminator) => match &terminator.kind {
263-
TerminatorKind::Call { func, args, .. } => self.validate_call(func, args),
269+
TerminatorKind::Call { func, args, .. } => {
270+
self.validate_call(func, args, loc.block)
271+
}
264272
TerminatorKind::Yield { .. } => Err(Unpromotable),
265273
kind => {
266274
span_bug!(terminator.source_info.span, "{:?} not promotable", kind);
@@ -587,42 +595,92 @@ impl<'tcx> Validator<'_, 'tcx> {
587595
Ok(())
588596
}
589597

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

597-
// Inside const/static items, we promote all (eligible) function calls.
598-
// Everywhere else, we require `#[rustc_promotable]` on the callee.
599-
let promote_all_const_fn = matches!(
658+
// Ideally, we'd stop here and reject the rest.
659+
// But for backward compatibility, we have to accept some promotion in const/static
660+
// initializers. Inline consts are explicitly excluded, they are more recent so we have no
661+
// backwards compatibility reason to allow more promotion inside of them.
662+
let promote_all_fn = matches!(
600663
self.const_kind,
601664
Some(hir::ConstContext::Static(_) | hir::ConstContext::Const { inline: false })
602665
);
603-
if !promote_all_const_fn {
604-
if let ty::FnDef(def_id, _) = *fn_ty.kind() {
605-
// Never promote runtime `const fn` calls of
606-
// functions without `#[rustc_promotable]`.
607-
if !self.tcx.is_promotable_const_fn(def_id) {
608-
return Err(Unpromotable);
609-
}
610-
}
666+
if !promote_all_fn {
667+
return Err(Unpromotable);
611668
}
612-
669+
// Make sure the callee is a `const fn`.
613670
let is_const_fn = match *fn_ty.kind() {
614671
ty::FnDef(def_id, _) => self.tcx.is_const_fn_raw(def_id),
615672
_ => false,
616673
};
617674
if !is_const_fn {
618675
return Err(Unpromotable);
619676
}
620-
621-
self.validate_operand(callee)?;
622-
for arg in args {
623-
self.validate_operand(&arg.node)?;
677+
// The problem is, this may promote calls to functions that panic.
678+
// We don't want to introduce compilation errors if there's a panic in a call in dead code.
679+
// So we ensure that this is not dead code.
680+
if !self.is_promotion_safe_block(block) {
681+
return Err(Unpromotable);
624682
}
625-
683+
// This passed all checks, so let's accept.
626684
Ok(())
627685
}
628686
}
@@ -633,7 +691,7 @@ fn validate_candidates(
633691
temps: &mut IndexSlice<Local, TempState>,
634692
candidates: &[Candidate],
635693
) -> Vec<Candidate> {
636-
let mut validator = Validator { ccx, temps };
694+
let mut validator = Validator { ccx, temps, promotion_safe_blocks: None };
637695

638696
candidates
639697
.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
@@ -41,6 +41,15 @@ const TEST_INTERIOR_MUT: () = {
4141

4242
const TEST_DROP: String = String::new();
4343

44+
// We do not promote function calls in `const` initializers in dead code.
45+
const fn mk_panic() -> u32 { panic!() }
46+
const fn mk_false() -> bool { false }
47+
const Y: () = {
48+
if mk_false() {
49+
let _x: &'static u32 = &mk_panic(); //~ ERROR temporary value dropped while borrowed
50+
}
51+
};
52+
4453
fn main() {
4554
// We must not promote things with interior mutability. Not even if we "project it away".
4655
let _val: &'static _ = &(Cell::new(1), 2).0; //~ ERROR temporary value dropped while borrowed

0 commit comments

Comments
 (0)