Skip to content

Commit

Permalink
Auto merge of rust-lang#121557 - RalfJung:const-fn-call-promotion, r=…
Browse files Browse the repository at this point in the history
…<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...

EDIT: Turns out that works. :) Here's the t-lang [nomination comment](rust-lang#121557 (comment)).

r? `@oli-obk`
  • Loading branch information
bors committed Mar 12, 2024
2 parents 7de1a1f + f205b14 commit 185afe0
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 249 deletions.
2 changes: 2 additions & 0 deletions compiler/rustc_mir_transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ fn mir_promoted(
body.tainted_by_errors = Some(error_reported);
}

// Collect `required_consts` *before* promotion, so if there are any consts being promoted
// we still add them to the list in the outer MIR body.
let mut required_consts = Vec::new();
let mut required_consts_visitor = RequiredConstsVisitor::new(&mut required_consts);
for (bb, bb_data) in traversal::reverse_postorder(&body) {
Expand Down
135 changes: 107 additions & 28 deletions compiler/rustc_mir_transform/src/promote_consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//! move analysis runs after promotion on broken MIR.

use either::{Left, Right};
use rustc_data_structures::fx::FxHashSet;
use rustc_hir as hir;
use rustc_middle::mir;
use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor};
Expand Down Expand Up @@ -60,7 +61,7 @@ impl<'tcx> MirPass<'tcx> for PromoteTemps<'tcx> {

let promotable_candidates = validate_candidates(&ccx, &mut temps, &all_candidates);

let promoted = promote_candidates(body, tcx, temps, promotable_candidates);
let promoted = promote_candidates(body, tcx, ccx.const_kind, temps, promotable_candidates);
self.promoted_fragments.set(promoted);
}
}
Expand Down Expand Up @@ -175,6 +176,12 @@ fn collect_temps_and_candidates<'tcx>(
struct Validator<'a, 'tcx> {
ccx: &'a ConstCx<'a, 'tcx>,
temps: &'a mut IndexSlice<Local, TempState>,
/// For backwards compatibility, we are promoting function calls in `const`/`static`
/// initializers. But we want to avoid evaluating code that might panic and that otherwise would
/// not have been evaluated, so we only promote such calls in basic blocks that are guaranteed
/// to execute. In other words, we only promote such calls in basic blocks that are definitely
/// not dead code. Here we cache the result of computing that set of basic blocks.
promotion_safe_blocks: Option<FxHashSet<BasicBlock>>,
}

impl<'a, 'tcx> std::ops::Deref for Validator<'a, 'tcx> {
Expand Down Expand Up @@ -260,7 +267,9 @@ impl<'tcx> Validator<'_, 'tcx> {
self.validate_rvalue(rhs)
}
Right(terminator) => match &terminator.kind {
TerminatorKind::Call { func, args, .. } => self.validate_call(func, args),
TerminatorKind::Call { func, args, .. } => {
self.validate_call(func, args, loc.block)
}
TerminatorKind::Yield { .. } => Err(Unpromotable),
kind => {
span_bug!(terminator.source_info.span, "{:?} not promotable", kind);
Expand Down Expand Up @@ -587,53 +596,110 @@ impl<'tcx> Validator<'_, 'tcx> {
Ok(())
}

/// Computes the sets of blocks of this MIR that are definitely going to be executed
/// if the function returns successfully. That makes it safe to promote calls in them
/// that might fail.
fn promotion_safe_blocks(body: &mir::Body<'tcx>) -> FxHashSet<BasicBlock> {
let mut safe_blocks = FxHashSet::default();
let mut safe_block = START_BLOCK;
loop {
safe_blocks.insert(safe_block);
// Let's see if we can find another safe block.
safe_block = match body.basic_blocks[safe_block].terminator().kind {
TerminatorKind::Goto { target } => target,
TerminatorKind::Call { target: Some(target), .. }
| TerminatorKind::Drop { target, .. } => {
// This calls a function or the destructor. `target` does not get executed if
// the callee loops or panics. But in both cases the const already fails to
// evaluate, so we are fine considering `target` a safe block for promotion.
target
}
TerminatorKind::Assert { target, .. } => {
// Similar to above, we only consider successful execution.
target
}
_ => {
// No next safe block.
break;
}
};
}
safe_blocks
}

/// Returns whether the block is "safe" for promotion, which means it cannot be dead code.
/// We use this to avoid promoting operations that can fail in dead code.
fn is_promotion_safe_block(&mut self, block: BasicBlock) -> bool {
let body = self.body;
let safe_blocks =
self.promotion_safe_blocks.get_or_insert_with(|| Self::promotion_safe_blocks(body));
safe_blocks.contains(&block)
}

fn validate_call(
&mut self,
callee: &Operand<'tcx>,
args: &[Spanned<Operand<'tcx>>],
block: BasicBlock,
) -> Result<(), Unpromotable> {
let fn_ty = callee.ty(self.body, self.tcx);
// Validate the operands. If they fail, there's no question -- we cannot promote.
self.validate_operand(callee)?;
for arg in args {
self.validate_operand(&arg.node)?;
}

// Inside const/static items, we promote all (eligible) function calls.
// Everywhere else, we require `#[rustc_promotable]` on the callee.
let promote_all_const_fn = matches!(
self.const_kind,
Some(hir::ConstContext::Static(_) | hir::ConstContext::Const { inline: false })
);
if !promote_all_const_fn {
if let ty::FnDef(def_id, _) = *fn_ty.kind() {
// Never promote runtime `const fn` calls of
// functions without `#[rustc_promotable]`.
if !self.tcx.is_promotable_const_fn(def_id) {
return Err(Unpromotable);
}
// Functions marked `#[rustc_promotable]` are explicitly allowed to be promoted, so we can
// accept them at this point.
let fn_ty = callee.ty(self.body, self.tcx);
if let ty::FnDef(def_id, _) = *fn_ty.kind() {
if self.tcx.is_promotable_const_fn(def_id) {
return Ok(());
}
}

// Ideally, we'd stop here and reject the rest. But for backward compatibility, we have to
// accept some promotion in const/static initializers.
if !promote_all_fn(self.ccx.const_kind) {
return Err(Unpromotable);
}
// Make sure the callee is a `const fn`.
let is_const_fn = match *fn_ty.kind() {
ty::FnDef(def_id, _) => self.tcx.is_const_fn_raw(def_id),
_ => false,
};
if !is_const_fn {
return Err(Unpromotable);
}

self.validate_operand(callee)?;
for arg in args {
self.validate_operand(&arg.node)?;
// The problem is, this may promote calls to functions that panic.
// We don't want to introduce compilation errors if there's a panic in a call in dead code.
// So we ensure that this is not dead code.
if !self.is_promotion_safe_block(block) {
return Err(Unpromotable);
}

// This passed all checks, so let's accept.
Ok(())
}
}

/// Decides whether in this context we are okay with promoting all functions (and not just
/// `#[rustc_promotable]`).
/// For backwards compatibility, we do that in static/const initializers.
fn promote_all_fn(const_kind: Option<hir::ConstContext>) -> bool {
// Inline consts are explicitly excluded, they are more recent so we have no
// backwards compatibility reason to allow more promotion inside of them.
matches!(
const_kind,
Some(hir::ConstContext::Static(_) | hir::ConstContext::Const { inline: false })
)
}

// FIXME(eddyb) remove the differences for promotability in `static`, `const`, `const fn`.
fn validate_candidates(
ccx: &ConstCx<'_, '_>,
temps: &mut IndexSlice<Local, TempState>,
candidates: &[Candidate],
) -> Vec<Candidate> {
let mut validator = Validator { ccx, temps };
let mut validator = Validator { ccx, temps, promotion_safe_blocks: None };

candidates
.iter()
Expand All @@ -652,6 +718,9 @@ struct Promoter<'a, 'tcx> {
/// If true, all nested temps are also kept in the
/// source MIR, not moved to the promoted MIR.
keep_original: bool,

/// If true, add the new const (the promoted) to the required_consts of the parent MIR.
add_to_required: bool,
}

impl<'a, 'tcx> Promoter<'a, 'tcx> {
Expand Down Expand Up @@ -798,11 +867,7 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
let args = tcx.erase_regions(GenericArgs::identity_for_item(tcx, def));
let uneval = mir::UnevaluatedConst { def, args, promoted: Some(promoted_id) };

Operand::Constant(Box::new(ConstOperand {
span,
user_ty: None,
const_: Const::Unevaluated(uneval, ty),
}))
ConstOperand { span, user_ty: None, const_: Const::Unevaluated(uneval, ty) }
};

let blocks = self.source.basic_blocks.as_mut();
Expand Down Expand Up @@ -838,11 +903,15 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
let promoted_ref = local_decls.push(promoted_ref);
assert_eq!(self.temps.push(TempState::Unpromotable), promoted_ref);

let promoted_operand = promoted_operand(ref_ty, span);
if self.add_to_required {
self.source.required_consts.push(promoted_operand);
}
let promoted_ref_statement = Statement {
source_info: statement.source_info,
kind: StatementKind::Assign(Box::new((
Place::from(promoted_ref),
Rvalue::Use(promoted_operand(ref_ty, span)),
Rvalue::Use(Operand::Constant(Box::new(promoted_operand))),
))),
};
self.extra_statements.push((loc, promoted_ref_statement));
Expand Down Expand Up @@ -885,6 +954,7 @@ impl<'a, 'tcx> MutVisitor<'tcx> for Promoter<'a, 'tcx> {
fn promote_candidates<'tcx>(
body: &mut Body<'tcx>,
tcx: TyCtxt<'tcx>,
ccx_const_kind: Option<hir::ConstContext>,
mut temps: IndexVec<Local, TempState>,
candidates: Vec<Candidate>,
) -> IndexVec<Promoted, Body<'tcx>> {
Expand Down Expand Up @@ -924,6 +994,11 @@ fn promote_candidates<'tcx>(
None,
body.tainted_by_errors,
);
// We keep `required_consts` of the new MIR body empty. All consts mentioned here have
// already been added to the parent MIR's `required_consts` (that is computed before
// promotion), and no matter where this promoted const ends up, our parent MIR must be
// somewhere in the reachable dependency chain so we can rely on its required consts being
// evaluated.
promoted.phase = MirPhase::Analysis(AnalysisPhase::Initial);

let promoter = Promoter {
Expand All @@ -933,6 +1008,10 @@ fn promote_candidates<'tcx>(
temps: &mut temps,
extra_statements: &mut extra_statements,
keep_original: false,
// If we promote all functions, some of these promoteds could fail, so we better make
// sure they get all checked as `required_consts`. Otherwise, as an optimization we
// *don't* add the promoteds to the parent's `required_consts`.
add_to_required: promote_all_fn(ccx_const_kind),
};

let mut promoted = promoter.promote_candidate(candidate, promotions.len());
Expand Down
44 changes: 0 additions & 44 deletions tests/ui/consts/const-eval/promoted_errors.noopt.stderr

This file was deleted.

44 changes: 0 additions & 44 deletions tests/ui/consts/const-eval/promoted_errors.opt.stderr

This file was deleted.

This file was deleted.

0 comments on commit 185afe0

Please sign in to comment.