Skip to content

Commit deac461

Browse files
committed
Auto merge of rust-lang#122819 - RalfJung:fn-call-promotion-experiment, r=<try>
experiment: never promote fn calls Cc rust-lang#80619 -- it's been a while since we did this experiment, let's get an idea for what the ecosystem looks like today.
2 parents 47dd709 + 12c433f commit deac461

27 files changed

+361
-398
lines changed

compiler/rustc_const_eval/src/const_eval/machine.rs

+5
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for CompileTimeInterpreter<'mir,
375375

376376
const PANIC_ON_ALLOC_FAIL: bool = false; // will be raised as a proper error
377377

378+
#[inline]
379+
fn all_required_consts_are_checked(_ecx: &InterpCx<'mir, 'tcx, Self>) -> bool {
380+
true
381+
}
382+
378383
#[inline(always)]
379384
fn enforce_alignment(ecx: &InterpCx<'mir, 'tcx, Self>) -> bool {
380385
matches!(ecx.machine.check_alignment, CheckAlignment::Error)

compiler/rustc_const_eval/src/interpret/eval_context.rs

+13-11
Original file line numberDiff line numberDiff line change
@@ -822,15 +822,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
822822
self.stack_mut().push(frame);
823823

824824
// Make sure all the constants required by this frame evaluate successfully (post-monomorphization check).
825-
if M::POST_MONO_CHECKS {
826-
for &const_ in &body.required_consts {
827-
let c = self
828-
.instantiate_from_current_frame_and_normalize_erasing_regions(const_.const_)?;
829-
c.eval(*self.tcx, self.param_env, const_.span).map_err(|err| {
830-
err.emit_note(*self.tcx);
831-
err
832-
})?;
833-
}
825+
for &const_ in &body.required_consts {
826+
let c =
827+
self.instantiate_from_current_frame_and_normalize_erasing_regions(const_.const_)?;
828+
c.eval(*self.tcx, self.param_env, const_.span).map_err(|err| {
829+
err.emit_note(*self.tcx);
830+
err
831+
})?;
834832
}
835833

836834
// done
@@ -1179,8 +1177,12 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
11791177
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
11801178
M::eval_mir_constant(self, *val, span, layout, |ecx, val, span, layout| {
11811179
let const_val = val.eval(*ecx.tcx, ecx.param_env, span).map_err(|err| {
1182-
// FIXME: somehow this is reachable even when POST_MONO_CHECKS is on.
1183-
// Are we not always populating `required_consts`?
1180+
if M::all_required_consts_are_checked(self)
1181+
&& !matches!(err, ErrorHandled::TooGeneric(..))
1182+
{
1183+
// Looks like the const is not captued by `required_consts`, that's bad.
1184+
bug!("interpret const eval failure of {val:?} which is not in required_consts");
1185+
}
11841186
err.emit_note(*ecx.tcx);
11851187
err
11861188
})?;

compiler/rustc_const_eval/src/interpret/machine.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized {
140140
/// Should the machine panic on allocation failures?
141141
const PANIC_ON_ALLOC_FAIL: bool;
142142

143-
/// Should post-monomorphization checks be run when a stack frame is pushed?
144-
const POST_MONO_CHECKS: bool = true;
143+
/// Determines whether `eval_mir_constant` can never fail because all required consts have
144+
/// already been checked before.
145+
fn all_required_consts_are_checked(ecx: &InterpCx<'mir, 'tcx, Self>) -> bool;
145146

146147
/// Whether memory accesses should be alignment-checked.
147148
fn enforce_alignment(ecx: &InterpCx<'mir, 'tcx, Self>) -> bool;

compiler/rustc_middle/src/mir/consts.rs

+18
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,24 @@ impl<'tcx> Const<'tcx> {
238238
}
239239
}
240240

241+
/// Determines whether we need to add this const to `required_consts`. This is the case if and
242+
/// only if evaluating it may error.
243+
#[inline]
244+
pub fn is_required_const(&self) -> bool {
245+
match self {
246+
Const::Ty(c) => match c.kind() {
247+
ty::ConstKind::Value(_) => false, // already a value, cannot error
248+
ty::ConstKind::Param(_) | ty::ConstKind::Error(_) => true, // these are errors or could be replaced by errors
249+
_ => bug!(
250+
"only ConstKind::Param/Value/Error should be encountered here, got {:#?}",
251+
c
252+
),
253+
},
254+
Const::Unevaluated(..) => true,
255+
Const::Val(..) => false, // already a value, cannot error
256+
}
257+
}
258+
241259
#[inline]
242260
pub fn try_to_scalar(self) -> Option<Scalar> {
243261
match self {

compiler/rustc_mir_transform/src/dataflow_const_prop.rs

+6
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,12 @@ impl<'mir, 'tcx: 'mir> rustc_const_eval::interpret::Machine<'mir, 'tcx> for Dumm
902902
type MemoryKind = !;
903903
const PANIC_ON_ALLOC_FAIL: bool = true;
904904

905+
#[inline(always)]
906+
fn all_required_consts_are_checked(_ecx: &InterpCx<'mir, 'tcx, Self>) -> bool {
907+
// We want to just eval random consts in the program, so `eval_mir_const` can fail.
908+
false
909+
}
910+
905911
#[inline(always)]
906912
fn enforce_alignment(_ecx: &InterpCx<'mir, 'tcx, Self>) -> bool {
907913
false // no reason to enforce alignment

compiler/rustc_mir_transform/src/inline.rs

+1-11
Original file line numberDiff line numberDiff line change
@@ -706,17 +706,7 @@ impl<'tcx> Inliner<'tcx> {
706706
});
707707

708708
// Copy only unevaluated constants from the callee_body into the caller_body.
709-
// Although we are only pushing `ConstKind::Unevaluated` consts to
710-
// `required_consts`, here we may not only have `ConstKind::Unevaluated`
711-
// because we are calling `instantiate_and_normalize_erasing_regions`.
712-
caller_body.required_consts.extend(callee_body.required_consts.iter().copied().filter(
713-
|&ct| match ct.const_ {
714-
Const::Ty(_) => {
715-
bug!("should never encounter ty::UnevaluatedConst in `required_consts`")
716-
}
717-
Const::Val(..) | Const::Unevaluated(..) => true,
718-
},
719-
));
709+
caller_body.required_consts.extend(callee_body.required_consts);
720710
}
721711

722712
fn make_call_args(

compiler/rustc_mir_transform/src/lib.rs

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

346+
// Collect `required_consts` *before* promotion, so if there are any consts being promoted
347+
// we still add them to the list in the outer MIR body.
346348
let mut required_consts = Vec::new();
347349
let mut required_consts_visitor = RequiredConstsVisitor::new(&mut required_consts);
348350
for (bb, bb_data) in traversal::reverse_postorder(&body) {

compiler/rustc_mir_transform/src/promote_consts.rs

+103-44
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
//! This pass assumes that every use is dominated by an
1212
//! initialization and can otherwise silence errors, if
1313
//! move analysis runs after promotion on broken MIR.
14+
#![allow(unused)]
1415

1516
use either::{Left, Right};
17+
use rustc_data_structures::fx::FxHashSet;
1618
use rustc_hir as hir;
1719
use rustc_middle::mir;
1820
use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor};
@@ -175,6 +177,12 @@ fn collect_temps_and_candidates<'tcx>(
175177
struct Validator<'a, 'tcx> {
176178
ccx: &'a ConstCx<'a, 'tcx>,
177179
temps: &'a mut IndexSlice<Local, TempState>,
180+
/// For backwards compatibility, we are promoting function calls in `const`/`static`
181+
/// initializers. But we want to avoid evaluating code that might panic and that otherwise would
182+
/// not have been evaluated, so we only promote such calls in basic blocks that are guaranteed
183+
/// to execute. In other words, we only promote such calls in basic blocks that are definitely
184+
/// not dead code. Here we cache the result of computing that set of basic blocks.
185+
promotion_safe_blocks: Option<FxHashSet<BasicBlock>>,
178186
}
179187

180188
impl<'a, 'tcx> std::ops::Deref for Validator<'a, 'tcx> {
@@ -260,7 +268,9 @@ impl<'tcx> Validator<'_, 'tcx> {
260268
self.validate_rvalue(rhs)
261269
}
262270
Right(terminator) => match &terminator.kind {
263-
TerminatorKind::Call { func, args, .. } => self.validate_call(func, args),
271+
TerminatorKind::Call { func, args, .. } => {
272+
self.validate_call(func, args, loc.block)
273+
}
264274
TerminatorKind::Yield { .. } => Err(Unpromotable),
265275
kind => {
266276
span_bug!(terminator.source_info.span, "{:?} not promotable", kind);
@@ -587,53 +597,78 @@ impl<'tcx> Validator<'_, 'tcx> {
587597
Ok(())
588598
}
589599

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+
590640
fn validate_call(
591641
&mut self,
592642
callee: &Operand<'tcx>,
593643
args: &[Spanned<Operand<'tcx>>],
644+
block: BasicBlock,
594645
) -> Result<(), Unpromotable> {
595-
let fn_ty = callee.ty(self.body, self.tcx);
596-
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!(
600-
self.const_kind,
601-
Some(hir::ConstContext::Static(_) | hir::ConstContext::Const { inline: false })
602-
);
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-
}
611-
}
612-
613-
let is_const_fn = match *fn_ty.kind() {
614-
ty::FnDef(def_id, _) => self.tcx.is_const_fn_raw(def_id),
615-
_ => false,
616-
};
617-
if !is_const_fn {
618-
return Err(Unpromotable);
619-
}
620-
646+
// Validate the operands. If they fail, there's no question -- we cannot promote.
621647
self.validate_operand(callee)?;
622648
for arg in args {
623649
self.validate_operand(&arg.node)?;
624650
}
625651

626-
Ok(())
652+
// Functions marked `#[rustc_promotable]` are explicitly allowed to be promoted, so we can
653+
// accept them at this point.
654+
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+
}
660+
661+
// Ideally, we'd stop here and reject the rest. So let's do that.
662+
return Err(Unpromotable);
627663
}
628664
}
629665

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

638673
candidates
639674
.iter()
@@ -652,6 +687,10 @@ struct Promoter<'a, 'tcx> {
652687
/// If true, all nested temps are also kept in the
653688
/// source MIR, not moved to the promoted MIR.
654689
keep_original: bool,
690+
691+
/// If true, add the new const (the promoted) to the required_consts of the parent MIR.
692+
/// This is initially false and then set by the visitor when it encounters a `Call` terminator.
693+
add_to_required: bool,
655694
}
656695

657696
impl<'a, 'tcx> Promoter<'a, 'tcx> {
@@ -754,6 +793,10 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
754793
TerminatorKind::Call {
755794
mut func, mut args, call_source: desugar, fn_span, ..
756795
} => {
796+
// This promoted involves a function call, so it may fail to evaluate.
797+
// Let's make sure it is added to `required_consts` so that that failure cannot get lost.
798+
self.add_to_required = true;
799+
757800
self.visit_operand(&mut func, loc);
758801
for arg in &mut args {
759802
self.visit_operand(&mut arg.node, loc);
@@ -788,7 +831,7 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
788831

789832
fn promote_candidate(mut self, candidate: Candidate, next_promoted_id: usize) -> Body<'tcx> {
790833
let def = self.source.source.def_id();
791-
let mut rvalue = {
834+
let (mut rvalue, promoted_op) = {
792835
let promoted = &mut self.promoted;
793836
let promoted_id = Promoted::new(next_promoted_id);
794837
let tcx = self.tcx;
@@ -798,11 +841,7 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
798841
let args = tcx.erase_regions(GenericArgs::identity_for_item(tcx, def));
799842
let uneval = mir::UnevaluatedConst { def, args, promoted: Some(promoted_id) };
800843

801-
Operand::Constant(Box::new(ConstOperand {
802-
span,
803-
user_ty: None,
804-
const_: Const::Unevaluated(uneval, ty),
805-
}))
844+
ConstOperand { span, user_ty: None, const_: Const::Unevaluated(uneval, ty) }
806845
};
807846

808847
let blocks = self.source.basic_blocks.as_mut();
@@ -838,22 +877,26 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
838877
let promoted_ref = local_decls.push(promoted_ref);
839878
assert_eq!(self.temps.push(TempState::Unpromotable), promoted_ref);
840879

880+
let promoted_operand = promoted_operand(ref_ty, span);
841881
let promoted_ref_statement = Statement {
842882
source_info: statement.source_info,
843883
kind: StatementKind::Assign(Box::new((
844884
Place::from(promoted_ref),
845-
Rvalue::Use(promoted_operand(ref_ty, span)),
885+
Rvalue::Use(Operand::Constant(Box::new(promoted_operand))),
846886
))),
847887
};
848888
self.extra_statements.push((loc, promoted_ref_statement));
849889

850-
Rvalue::Ref(
851-
tcx.lifetimes.re_erased,
852-
*borrow_kind,
853-
Place {
854-
local: mem::replace(&mut place.local, promoted_ref),
855-
projection: List::empty(),
856-
},
890+
(
891+
Rvalue::Ref(
892+
tcx.lifetimes.re_erased,
893+
*borrow_kind,
894+
Place {
895+
local: mem::replace(&mut place.local, promoted_ref),
896+
projection: List::empty(),
897+
},
898+
),
899+
promoted_operand,
857900
)
858901
};
859902

@@ -865,6 +908,12 @@ impl<'a, 'tcx> Promoter<'a, 'tcx> {
865908

866909
let span = self.promoted.span;
867910
self.assign(RETURN_PLACE, rvalue, span);
911+
912+
// Now that we did promotion, we know whether we'll want to add this to `required_consts`.
913+
if self.add_to_required {
914+
self.source.required_consts.push(promoted_op);
915+
}
916+
868917
self.promoted
869918
}
870919
}
@@ -880,6 +929,14 @@ impl<'a, 'tcx> MutVisitor<'tcx> for Promoter<'a, 'tcx> {
880929
*local = self.promote_temp(*local);
881930
}
882931
}
932+
933+
fn visit_constant(&mut self, constant: &mut ConstOperand<'tcx>, _location: Location) {
934+
if constant.const_.is_required_const() {
935+
self.promoted.required_consts.push(*constant);
936+
}
937+
938+
// Skipping `super_constant` as the visitor is otherwise only looking for locals.
939+
}
883940
}
884941

885942
fn promote_candidates<'tcx>(
@@ -933,8 +990,10 @@ fn promote_candidates<'tcx>(
933990
temps: &mut temps,
934991
extra_statements: &mut extra_statements,
935992
keep_original: false,
993+
add_to_required: false,
936994
};
937995

996+
// `required_consts` of the promoted itself gets filled while building the MIR body.
938997
let mut promoted = promoter.promote_candidate(candidate, promotions.len());
939998
promoted.source.promoted = Some(promotions.next_index());
940999
promotions.push(promoted);

0 commit comments

Comments
 (0)