Skip to content

Commit b8727e2

Browse files
committed
Prevent query cycles during inlining
1 parent 4d0dd02 commit b8727e2

File tree

9 files changed

+357
-17
lines changed

9 files changed

+357
-17
lines changed

compiler/rustc_middle/src/query/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,27 @@ rustc_queries! {
782782
}
783783

784784
Other {
785+
/// Check whether the function has any recursion that could cause the inliner to trigger
786+
/// a cycle. Returns the call stack causing the cycle. The call stack does not contain the
787+
/// current function, just all intermediate functions.
788+
query mir_callgraph_reachable(key: (ty::Instance<'tcx>, LocalDefId)) -> bool {
789+
fatal_cycle
790+
desc { |tcx|
791+
"computing if `{}` (transitively) calls `{}`",
792+
key.0,
793+
tcx.def_path_str(key.1.to_def_id()),
794+
}
795+
}
796+
797+
/// Obtain all the calls into other local functions
798+
query mir_inliner_callees(key: ty::InstanceDef<'tcx>) -> &'tcx [(DefId, SubstsRef<'tcx>)] {
799+
fatal_cycle
800+
desc { |tcx|
801+
"computing all local function calls in `{}`",
802+
tcx.def_path_str(key.def_id()),
803+
}
804+
}
805+
785806
/// Evaluates a constant and returns the computed allocation.
786807
///
787808
/// **Do not use this** directly, use the `tcx.eval_static_initializer` wrapper.

compiler/rustc_middle/src/ty/query/keys.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ impl Key for (DefId, DefId) {
127127
}
128128
}
129129

130+
impl Key for (ty::Instance<'tcx>, LocalDefId) {
131+
type CacheSelector = DefaultCacheSelector;
132+
133+
fn query_crate(&self) -> CrateNum {
134+
self.0.query_crate()
135+
}
136+
fn default_span(&self, tcx: TyCtxt<'_>) -> Span {
137+
self.0.default_span(tcx)
138+
}
139+
}
140+
130141
impl Key for (DefId, LocalDefId) {
131142
type CacheSelector = DefaultCacheSelector;
132143

compiler/rustc_mir/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pub fn provide(providers: &mut Providers) {
5757
providers.eval_to_const_value_raw = const_eval::eval_to_const_value_raw_provider;
5858
providers.eval_to_allocation_raw = const_eval::eval_to_allocation_raw_provider;
5959
providers.const_caller_location = const_eval::const_caller_location;
60+
providers.mir_callgraph_reachable = transform::inline::cycle::mir_callgraph_reachable;
61+
providers.mir_inliner_callees = transform::inline::cycle::mir_inliner_callees;
6062
providers.destructure_const = |tcx, param_env_and_value| {
6163
let (param_env, value) = param_env_and_value.into_parts();
6264
const_eval::destructure_const(tcx, param_env, value)

compiler/rustc_mir/src/transform/inline.rs

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::transform::MirPass;
1717
use std::iter;
1818
use std::ops::{Range, RangeFrom};
1919

20+
crate mod cycle;
21+
2022
const INSTR_COST: usize = 5;
2123
const CALL_PENALTY: usize = 25;
2224
const LANDINGPAD_PENALTY: usize = 50;
@@ -50,6 +52,8 @@ impl<'tcx> MirPass<'tcx> for Inline {
5052
return;
5153
}
5254

55+
let span = trace_span!("inline", body = %tcx.def_path_str(body.source.def_id()));
56+
let _guard = span.enter();
5357
if inline(tcx, body) {
5458
debug!("running simplify cfg on {:?}", body.source);
5559
CfgSimplifier::new(body).simplify();
@@ -90,8 +94,8 @@ struct Inliner<'tcx> {
9094
codegen_fn_attrs: &'tcx CodegenFnAttrs,
9195
/// Caller HirID.
9296
hir_id: hir::HirId,
93-
/// Stack of inlined instances.
94-
history: Vec<Instance<'tcx>>,
97+
/// Stack of inlined Instances.
98+
history: Vec<ty::Instance<'tcx>>,
9599
/// Indicates that the caller body has been modified.
96100
changed: bool,
97101
}
@@ -103,13 +107,28 @@ impl Inliner<'tcx> {
103107
None => continue,
104108
Some(it) => it,
105109
};
110+
let span = trace_span!("process_blocks", %callsite.callee, ?bb);
111+
let _guard = span.enter();
112+
113+
trace!(
114+
"checking for self recursion ({:?} vs body_source: {:?})",
115+
callsite.callee.def_id(),
116+
caller_body.source.def_id()
117+
);
118+
if callsite.callee.def_id() == caller_body.source.def_id() {
119+
debug!("Not inlining a function into itself");
120+
continue;
121+
}
106122

107-
if !self.is_mir_available(&callsite.callee, caller_body) {
123+
if !self.is_mir_available(callsite.callee, caller_body) {
108124
debug!("MIR unavailable {}", callsite.callee);
109125
continue;
110126
}
111127

128+
let span = trace_span!("instance_mir", %callsite.callee);
129+
let instance_mir_guard = span.enter();
112130
let callee_body = self.tcx.instance_mir(callsite.callee.def);
131+
drop(instance_mir_guard);
113132
if !self.should_inline(callsite, callee_body) {
114133
continue;
115134
}
@@ -137,28 +156,61 @@ impl Inliner<'tcx> {
137156
}
138157
}
139158

140-
fn is_mir_available(&self, callee: &Instance<'tcx>, caller_body: &Body<'tcx>) -> bool {
141-
if let InstanceDef::Item(_) = callee.def {
142-
if !self.tcx.is_mir_available(callee.def_id()) {
143-
return false;
159+
#[instrument(skip(self, caller_body))]
160+
fn is_mir_available(&self, callee: Instance<'tcx>, caller_body: &Body<'tcx>) -> bool {
161+
match callee.def {
162+
InstanceDef::Item(_) => {
163+
// If there is no MIR available (either because it was not in metadata or
164+
// because it has no MIR because it's an extern function), then the inliner
165+
// won't cause cycles on this.
166+
if !self.tcx.is_mir_available(callee.def_id()) {
167+
return false;
168+
}
144169
}
170+
// These have no own callable MIR.
171+
InstanceDef::Intrinsic(_) | InstanceDef::Virtual(..) => return false,
172+
// This cannot result in an immediate cycle since the callee MIR is a shim, which does
173+
// not get any optimizations run on it. Any subsequent inlining may cause cycles, but we
174+
// do not need to catch this here, we can wait until the inliner decides to continue
175+
// inlining a second time.
176+
InstanceDef::VtableShim(_)
177+
| InstanceDef::ReifyShim(_)
178+
| InstanceDef::FnPtrShim(..)
179+
| InstanceDef::ClosureOnceShim { .. }
180+
| InstanceDef::DropGlue(..)
181+
| InstanceDef::CloneShim(..) => return true,
182+
}
183+
184+
if self.tcx.is_constructor(callee.def_id()) {
185+
trace!("constructors always have MIR");
186+
// Constructor functions cannot cause a query cycle.
187+
return true;
145188
}
146189

147190
if let Some(callee_def_id) = callee.def_id().as_local() {
148191
let callee_hir_id = self.tcx.hir().local_def_id_to_hir_id(callee_def_id);
149-
// Avoid a cycle here by only using `instance_mir` only if we have
150-
// a lower `HirId` than the callee. This ensures that the callee will
151-
// not inline us. This trick only works without incremental compilation.
152-
// So don't do it if that is enabled. Also avoid inlining into generators,
192+
// Avoid inlining into generators,
153193
// since their `optimized_mir` is used for layout computation, which can
154194
// create a cycle, even when no attempt is made to inline the function
155195
// in the other direction.
156-
!self.tcx.dep_graph.is_fully_enabled()
196+
caller_body.generator_kind.is_none()
197+
&& (
198+
// Avoid a cycle here by only using `instance_mir` only if we have
199+
// a lower `HirId` than the callee. This ensures that the callee will
200+
// not inline us. This trick only works without incremental compilation.
201+
// So don't do it if that is enabled.
202+
!self.tcx.dep_graph.is_fully_enabled()
157203
&& self.hir_id < callee_hir_id
158-
&& caller_body.generator_kind.is_none()
204+
// If we know for sure that the function we're calling will itself try to
205+
// call us, then we avoid inlining that function.
206+
|| !self.tcx.mir_callgraph_reachable((callee, caller_body.source.def_id().expect_local()))
207+
)
159208
} else {
160-
// This cannot result in a cycle since the callee MIR is from another crate
161-
// and is already optimized.
209+
// This cannot result in an immediate cycle since the callee MIR is from another crate
210+
// and is already optimized. Any subsequent inlining may cause cycles, but we do
211+
// not need to catch this here, we can wait until the inliner decides to continue
212+
// inlining a second time.
213+
trace!("functions from other crates always have MIR");
162214
true
163215
}
164216
}
@@ -203,8 +255,8 @@ impl Inliner<'tcx> {
203255
None
204256
}
205257

258+
#[instrument(skip(self, callee_body))]
206259
fn should_inline(&self, callsite: CallSite<'tcx>, callee_body: &Body<'tcx>) -> bool {
207-
debug!("should_inline({:?})", callsite);
208260
let tcx = self.tcx;
209261

210262
if callsite.fn_sig.c_variadic() {
@@ -333,7 +385,9 @@ impl Inliner<'tcx> {
333385
if let Ok(Some(instance)) =
334386
Instance::resolve(self.tcx, self.param_env, def_id, substs)
335387
{
336-
if callsite.callee == instance || self.history.contains(&instance) {
388+
if callsite.callee.def_id() == instance.def_id()
389+
|| self.history.contains(&instance)
390+
{
337391
debug!("`callee is recursive - not inlining");
338392
return false;
339393
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
2+
use rustc_data_structures::stack::ensure_sufficient_stack;
3+
use rustc_hir::def_id::{DefId, LocalDefId};
4+
use rustc_middle::mir::TerminatorKind;
5+
use rustc_middle::ty::TypeFoldable;
6+
use rustc_middle::ty::{self, subst::SubstsRef, InstanceDef, TyCtxt};
7+
8+
// FIXME: check whether it is cheaper to precompute the entire call graph instead of invoking
9+
// this query riddiculously often.
10+
#[instrument(skip(tcx, root, target))]
11+
crate fn mir_callgraph_reachable(
12+
tcx: TyCtxt<'tcx>,
13+
(root, target): (ty::Instance<'tcx>, LocalDefId),
14+
) -> bool {
15+
trace!(%root, target = %tcx.def_path_str(target.to_def_id()));
16+
let param_env = tcx.param_env_reveal_all_normalized(target);
17+
assert_ne!(
18+
root.def_id().expect_local(),
19+
target,
20+
"you should not call `mir_callgraph_reachable` on immediate self recursion"
21+
);
22+
assert!(
23+
matches!(root.def, InstanceDef::Item(_)),
24+
"you should not call `mir_callgraph_reachable` on shims"
25+
);
26+
assert!(
27+
!tcx.is_constructor(root.def_id()),
28+
"you should not call `mir_callgraph_reachable` on enum/struct constructor functions"
29+
);
30+
#[instrument(skip(tcx, param_env, target, stack, seen, recursion_limiter, caller))]
31+
fn process(
32+
tcx: TyCtxt<'tcx>,
33+
param_env: ty::ParamEnv<'tcx>,
34+
caller: ty::Instance<'tcx>,
35+
target: LocalDefId,
36+
stack: &mut Vec<ty::Instance<'tcx>>,
37+
seen: &mut FxHashSet<ty::Instance<'tcx>>,
38+
recursion_limiter: &mut FxHashMap<DefId, usize>,
39+
) -> bool {
40+
trace!(%caller);
41+
for &(callee, substs) in tcx.mir_inliner_callees(caller.def) {
42+
let substs = caller.subst_mir_and_normalize_erasing_regions(tcx, param_env, substs);
43+
let callee = match ty::Instance::resolve(tcx, param_env, callee, substs).unwrap() {
44+
Some(callee) => callee,
45+
None => {
46+
trace!(?callee, "cannot resolve, skipping");
47+
continue;
48+
}
49+
};
50+
51+
// Found a path.
52+
if callee.def_id() == target.to_def_id() {
53+
return true;
54+
}
55+
56+
if tcx.is_constructor(callee.def_id()) {
57+
trace!("constructors always have MIR");
58+
// Constructor functions cannot cause a query cycle.
59+
continue;
60+
}
61+
62+
match callee.def {
63+
InstanceDef::Item(_) => {
64+
// If there is no MIR available (either because it was not in metadata or
65+
// because it has no MIR because it's an extern function), then the inliner
66+
// won't cause cycles on this.
67+
if !tcx.is_mir_available(callee.def_id()) {
68+
trace!(?callee, "no mir available, skipping");
69+
continue;
70+
}
71+
}
72+
// These have no own callable MIR.
73+
InstanceDef::Intrinsic(_) | InstanceDef::Virtual(..) => continue,
74+
// These have MIR and if that MIR is inlined, substituted and then inlining is run
75+
// again, a function item can end up getting inlined. Thus we'll be able to cause
76+
// a cycle that way
77+
InstanceDef::VtableShim(_)
78+
| InstanceDef::ReifyShim(_)
79+
| InstanceDef::FnPtrShim(..)
80+
| InstanceDef::ClosureOnceShim { .. }
81+
| InstanceDef::CloneShim(..) => {}
82+
InstanceDef::DropGlue(..) => {
83+
// FIXME: A not fully substituted drop shim can cause ICEs if one attempts to
84+
// have its MIR built. Likely oli-obk just screwed up the `ParamEnv`s, so this
85+
// needs some more analysis.
86+
if callee.needs_subst() {
87+
continue;
88+
}
89+
}
90+
}
91+
92+
if seen.insert(callee) {
93+
let recursion = recursion_limiter.entry(callee.def_id()).or_default();
94+
trace!(?callee, recursion = *recursion);
95+
if tcx.sess.recursion_limit().value_within_limit(*recursion) {
96+
*recursion += 1;
97+
stack.push(callee);
98+
let found_recursion = ensure_sufficient_stack(|| {
99+
process(tcx, param_env, callee, target, stack, seen, recursion_limiter)
100+
});
101+
if found_recursion {
102+
return true;
103+
}
104+
stack.pop();
105+
} else {
106+
// Pessimistically assume that there could be recursion.
107+
return true;
108+
}
109+
}
110+
}
111+
false
112+
}
113+
process(
114+
tcx,
115+
param_env,
116+
root,
117+
target,
118+
&mut Vec::new(),
119+
&mut FxHashSet::default(),
120+
&mut FxHashMap::default(),
121+
)
122+
}
123+
124+
crate fn mir_inliner_callees<'tcx>(
125+
tcx: TyCtxt<'tcx>,
126+
instance: ty::InstanceDef<'tcx>,
127+
) -> &'tcx [(DefId, SubstsRef<'tcx>)] {
128+
let steal;
129+
let guard;
130+
let body = match (instance, instance.def_id().as_local()) {
131+
(InstanceDef::Item(_), Some(def_id)) => {
132+
let def = ty::WithOptConstParam::unknown(def_id);
133+
steal = tcx.mir_promoted(def).0;
134+
guard = steal.borrow();
135+
&*guard
136+
}
137+
// Functions from other crates and MIR shims
138+
_ => tcx.instance_mir(instance),
139+
};
140+
let mut calls = Vec::new();
141+
for bb_data in body.basic_blocks() {
142+
let terminator = bb_data.terminator();
143+
if let TerminatorKind::Call { func, .. } = &terminator.kind {
144+
let ty = func.ty(&body.local_decls, tcx);
145+
let call = match ty.kind() {
146+
ty::FnDef(def_id, substs) => (*def_id, *substs),
147+
_ => continue,
148+
};
149+
// We've seen this before
150+
if calls.contains(&call) {
151+
continue;
152+
}
153+
calls.push(call);
154+
}
155+
}
156+
tcx.arena.alloc_slice(&calls)
157+
}

compiler/rustc_mir/src/transform/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,15 @@ fn mir_drops_elaborated_and_const_checked<'tcx>(
419419
tcx.ensure().mir_borrowck(def.did);
420420
}
421421

422+
let hir_id = tcx.hir().local_def_id_to_hir_id(def.did);
423+
use rustc_middle::hir::map::blocks::FnLikeNode;
424+
let is_fn_like = FnLikeNode::from_node(tcx.hir().get(hir_id)).is_some();
425+
if is_fn_like {
426+
let did = def.did.to_def_id();
427+
let def = ty::WithOptConstParam::unknown(did);
428+
let _ = tcx.mir_inliner_callees(ty::InstanceDef::Item(def));
429+
}
430+
422431
let (body, _) = tcx.mir_promoted(def);
423432
let mut body = body.steal();
424433

0 commit comments

Comments
 (0)