Skip to content

Commit cda25e5

Browse files
committed
Refactor & fixup interpreter implementation of tail calls
1 parent 30b18d7 commit cda25e5

File tree

4 files changed

+162
-70
lines changed

4 files changed

+162
-70
lines changed

compiler/rustc_const_eval/src/interpret/eval_context.rs

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,13 @@ pub enum StackPopCleanup {
159159
Root { cleanup: bool },
160160
}
161161

162+
/// Return type of [`InterpCx::pop_stack_frame`].
163+
pub struct StackPop<'tcx, Prov: Provenance> {
164+
pub jump: StackPopJump,
165+
pub target: StackPopCleanup,
166+
pub destination: MPlaceTy<'tcx, Prov>,
167+
}
168+
162169
/// State of a local variable including a memoized layout
163170
#[derive(Clone)]
164171
pub struct LocalState<'tcx, Prov: Provenance = CtfeProvenance> {
@@ -803,14 +810,31 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
803810
return_to_block: StackPopCleanup,
804811
) -> InterpResult<'tcx> {
805812
trace!("body: {:#?}", body);
813+
814+
// First push a stack frame so we have access to the local args
815+
self.push_new_stack_frame(instance, body, return_to_block, return_place.clone())?;
816+
817+
self.after_stack_frame_push(instance, body)?;
818+
819+
Ok(())
820+
}
821+
822+
/// Creates a new stack frame, initializes it and pushes it unto the stack.
823+
/// A private helper for [`push_stack_frame`](InterpCx::push_stack_frame).
824+
fn push_new_stack_frame(
825+
&mut self,
826+
instance: ty::Instance<'tcx>,
827+
body: &'tcx mir::Body<'tcx>,
828+
return_to_block: StackPopCleanup,
829+
return_place: MPlaceTy<'tcx, M::Provenance>,
830+
) -> InterpResult<'tcx> {
806831
let dead_local = LocalState { value: LocalValue::Dead, layout: Cell::new(None) };
807832
let locals = IndexVec::from_elem(dead_local, &body.local_decls);
808-
// First push a stack frame so we have access to the local args
809833
let pre_frame = Frame {
810834
body,
811835
loc: Right(body.span), // Span used for errors caused during preamble.
812836
return_to_block,
813-
return_place: return_place.clone(),
837+
return_place,
814838
locals,
815839
instance,
816840
tracing_span: SpanGuard::new(),
@@ -819,6 +843,15 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
819843
let frame = M::init_frame(self, pre_frame)?;
820844
self.stack_mut().push(frame);
821845

846+
Ok(())
847+
}
848+
849+
/// A private helper for [`push_stack_frame`](InterpCx::push_stack_frame).
850+
fn after_stack_frame_push(
851+
&mut self,
852+
instance: ty::Instance<'tcx>,
853+
body: &'tcx mir::Body<'tcx>,
854+
) -> InterpResult<'tcx> {
822855
// Make sure all the constants required by this frame evaluate successfully (post-monomorphization check).
823856
for &const_ in &body.required_consts {
824857
let c =
@@ -839,6 +872,59 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
839872
Ok(())
840873
}
841874

875+
/// Pops a stack frame from the stack and returns some information about it.
876+
///
877+
/// This also deallocates locals, if necessary.
878+
///
879+
/// [`M::before_stack_pop`] should be called before calling this function.
880+
/// [`M::after_stack_pop`] is called by this function automatically.
881+
///
882+
/// [`M::before_stack_pop`]: Machine::before_stack_pop
883+
/// [`M::after_stack_pop`]: Machine::after_stack_pop
884+
pub fn pop_stack_frame(
885+
&mut self,
886+
unwinding: bool,
887+
) -> InterpResult<'tcx, StackPop<'tcx, M::Provenance>> {
888+
let cleanup = self.cleanup_current_frame_locals()?;
889+
890+
let frame =
891+
self.stack_mut().pop().expect("tried to pop a stack frame, but there were none");
892+
893+
let target = frame.return_to_block;
894+
let destination = frame.return_place.clone();
895+
896+
let jump = if cleanup {
897+
M::after_stack_pop(self, frame, unwinding)?
898+
} else {
899+
StackPopJump::NoCleanup
900+
};
901+
902+
Ok(StackPop { jump, target, destination })
903+
}
904+
905+
/// A private helper for [`push_stack_frame`](InterpCx::push_stack_frame).
906+
/// Returns whatever the cleanup was done.
907+
fn cleanup_current_frame_locals(&mut self) -> InterpResult<'tcx, bool> {
908+
// Cleanup: deallocate locals.
909+
// Usually we want to clean up (deallocate locals), but in a few rare cases we don't.
910+
// We do this while the frame is still on the stack, so errors point to the callee.
911+
let return_to_block = self.frame().return_to_block;
912+
let cleanup = match return_to_block {
913+
StackPopCleanup::Goto { .. } => true,
914+
StackPopCleanup::Root { cleanup, .. } => cleanup,
915+
};
916+
917+
if cleanup {
918+
// We need to take the locals out, since we need to mutate while iterating.
919+
let locals = mem::take(&mut self.frame_mut().locals);
920+
for local in &locals {
921+
self.deallocate_local(local.value)?;
922+
}
923+
}
924+
925+
Ok(cleanup)
926+
}
927+
842928
/// Jump to the given block.
843929
#[inline]
844930
pub fn go_to_block(&mut self, target: mir::BasicBlock) {
@@ -886,7 +972,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
886972
}
887973

888974
/// Pops the current frame from the stack, deallocating the
889-
/// memory for allocated locals.
975+
/// memory for allocated locals, and jumps to an appropriate place.
890976
///
891977
/// If `unwinding` is `false`, then we are performing a normal return
892978
/// from a function. In this case, we jump back into the frame of the caller,
@@ -899,7 +985,10 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
899985
/// The cleanup block ends with a special `Resume` terminator, which will
900986
/// cause us to continue unwinding.
901987
#[instrument(skip(self), level = "debug")]
902-
pub(super) fn pop_stack_frame(&mut self, unwinding: bool) -> InterpResult<'tcx> {
988+
pub(super) fn return_from_current_stack_frame(
989+
&mut self,
990+
unwinding: bool,
991+
) -> InterpResult<'tcx> {
903992
info!(
904993
"popping stack frame ({})",
905994
if unwinding { "during unwinding" } else { "returning from function" }
@@ -947,45 +1036,31 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
9471036
Ok(())
9481037
};
9491038

950-
// Cleanup: deallocate locals.
951-
// Usually we want to clean up (deallocate locals), but in a few rare cases we don't.
952-
// We do this while the frame is still on the stack, so errors point to the callee.
953-
let return_to_block = self.frame().return_to_block;
954-
let cleanup = match return_to_block {
955-
StackPopCleanup::Goto { .. } => true,
956-
StackPopCleanup::Root { cleanup, .. } => cleanup,
957-
};
958-
if cleanup {
959-
// We need to take the locals out, since we need to mutate while iterating.
960-
let locals = mem::take(&mut self.frame_mut().locals);
961-
for local in &locals {
962-
self.deallocate_local(local.value)?;
963-
}
964-
}
965-
9661039
// All right, now it is time to actually pop the frame.
967-
// Note that its locals are gone already, but that's fine.
968-
let frame =
969-
self.stack_mut().pop().expect("tried to pop a stack frame, but there were none");
1040+
let frame = self.pop_stack_frame(unwinding)?;
1041+
9701042
// Report error from return value copy, if any.
9711043
copy_ret_result?;
9721044

973-
// If we are not doing cleanup, also skip everything else.
974-
if !cleanup {
975-
assert!(self.stack().is_empty(), "only the topmost frame should ever be leaked");
976-
assert!(!unwinding, "tried to skip cleanup during unwinding");
977-
// Skip machine hook.
978-
return Ok(());
979-
}
980-
if M::after_stack_pop(self, frame, unwinding)? == StackPopJump::NoJump {
981-
// The hook already did everything.
982-
return Ok(());
1045+
match frame.jump {
1046+
StackPopJump::Normal => {}
1047+
StackPopJump::NoJump => {
1048+
// The hook already did everything.
1049+
return Ok(());
1050+
}
1051+
StackPopJump::NoCleanup => {
1052+
// If we are not doing cleanup, also skip everything else.
1053+
assert!(self.stack().is_empty(), "only the topmost frame should ever be leaked");
1054+
assert!(!unwinding, "tried to skip cleanup during unwinding");
1055+
// Skip machine hook.
1056+
return Ok(());
1057+
}
9831058
}
9841059

9851060
// Normal return, figure out where to jump.
9861061
if unwinding {
9871062
// Follow the unwind edge.
988-
let unwind = match return_to_block {
1063+
let unwind = match frame.target {
9891064
StackPopCleanup::Goto { unwind, .. } => unwind,
9901065
StackPopCleanup::Root { .. } => {
9911066
panic!("encountered StackPopCleanup::Root when unwinding!")
@@ -995,7 +1070,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
9951070
self.unwind_to_block(unwind)
9961071
} else {
9971072
// Follow the normal return edge.
998-
match return_to_block {
1073+
match frame.target {
9991074
StackPopCleanup::Goto { ret, .. } => self.return_to_block(ret),
10001075
StackPopCleanup::Root { .. } => {
10011076
assert!(

compiler/rustc_const_eval/src/interpret/machine.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub enum StackPopJump {
3636
/// Indicates that we should *not* jump to the return/unwind address, as the callback already
3737
/// took care of everything.
3838
NoJump,
39+
40+
/// Returned by [`InterpCx::pop_stack_frame`] when no cleanup should be done.
41+
NoCleanup,
3942
}
4043

4144
/// Whether this kind of memory is allowed to leak

compiler/rustc_const_eval/src/interpret/step.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
3232
// We are unwinding and this fn has no cleanup code.
3333
// Just go on unwinding.
3434
trace!("unwinding: skipping frame");
35-
self.pop_stack_frame(/* unwinding */ true)?;
35+
self.return_from_current_stack_frame(/* unwinding */ true)?;
3636
return Ok(true);
3737
};
3838
let basic_block = &self.body().basic_blocks[loc.block];

compiler/rustc_const_eval/src/interpret/terminator.rs

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use either::Either;
44
use rustc_middle::ty::TyCtxt;
55
use tracing::trace;
66

7-
use rustc_middle::span_bug;
87
use rustc_middle::{
9-
mir,
8+
bug, mir, span_bug,
109
ty::{
1110
self,
1211
layout::{FnAbiOf, IntegerExt, LayoutOf, TyAndLayout},
@@ -26,7 +25,10 @@ use super::{
2625
InterpResult, MPlaceTy, Machine, OpTy, PlaceTy, Projectable, Provenance, Scalar,
2726
StackPopCleanup,
2827
};
29-
use crate::fluent_generated as fluent;
28+
use crate::{
29+
fluent_generated as fluent,
30+
interpret::{eval_context::StackPop, StackPopJump},
31+
};
3032

3133
/// An argment passed to a function.
3234
#[derive(Clone, Debug)]
@@ -93,7 +95,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
9395
use rustc_middle::mir::TerminatorKind::*;
9496
match terminator.kind {
9597
Return => {
96-
self.pop_stack_frame(/* unwinding */ false)?
98+
self.return_from_current_stack_frame(/* unwinding */ false)?
9799
}
98100

99101
Goto { target } => self.go_to_block(target),
@@ -160,35 +162,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
160162
let EvaluatedCalleeAndArgs { callee, args, fn_sig, fn_abi, with_caller_location } =
161163
self.eval_callee_and_args(terminator, func, args)?;
162164

163-
// This is the "canonical" implementation of tails calls,
164-
// a pop of the current stack frame, followed by a normal call
165-
// which pushes a new stack frame, with the return address from
166-
// the popped stack frame.
167-
//
168-
// Note that we can't use `pop_stack_frame` as it "executes"
169-
// the goto to the return block, but we don't want to,
170-
// only the tail called function should return to the current
171-
// return block.
172-
let Some(prev_frame) = self.stack_mut().pop() else {
173-
span_bug!(
174-
terminator.source_info.span,
175-
"empty stack while evaluating this tail call"
176-
)
177-
};
178-
179-
let StackPopCleanup::Goto { ret, unwind } = prev_frame.return_to_block else {
180-
span_bug!(terminator.source_info.span, "tail call with the root stack frame")
181-
};
182-
183-
self.eval_fn_call(
184-
callee,
185-
(fn_sig.abi, fn_abi),
186-
&args,
187-
with_caller_location,
188-
&prev_frame.return_place,
189-
ret,
190-
unwind,
191-
)?;
165+
self.eval_fn_tail_call(callee, (fn_sig.abi, fn_abi), &args, with_caller_location)?;
192166

193167
if self.frame_idx() != old_frame_idx {
194168
span_bug!(
@@ -235,7 +209,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
235209
trace!("unwinding: resuming from cleanup");
236210
// By definition, a Resume terminator means
237211
// that we're unwinding
238-
self.pop_stack_frame(/* unwinding */ true)?;
212+
self.return_from_current_stack_frame(/* unwinding */ true)?;
239213
return Ok(());
240214
}
241215

@@ -989,6 +963,46 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
989963
}
990964
}
991965

966+
pub(crate) fn eval_fn_tail_call(
967+
&mut self,
968+
fn_val: FnVal<'tcx, M::ExtraFnVal>,
969+
(caller_abi, caller_fn_abi): (Abi, &FnAbi<'tcx, Ty<'tcx>>),
970+
args: &[FnArg<'tcx, M::Provenance>],
971+
with_caller_location: bool,
972+
) -> InterpResult<'tcx> {
973+
trace!("eval_fn_call: {:#?}", fn_val);
974+
975+
// This is the "canonical" implementation of tails calls,
976+
// a pop of the current stack frame, followed by a normal call
977+
// which pushes a new stack frame, with the return address from
978+
// the popped stack frame.
979+
//
980+
// Note that we can't use `pop_stack_frame` as it "executes"
981+
// the goto to the return block, but we don't want to,
982+
// only the tail called function should return to the current
983+
// return block.
984+
985+
M::before_stack_pop(self, self.frame())?;
986+
987+
let StackPop { jump, target, destination } = self.pop_stack_frame(false)?;
988+
989+
assert_eq!(jump, StackPopJump::Normal);
990+
991+
let StackPopCleanup::Goto { ret, unwind } = target else {
992+
bug!("can't tailcall as root");
993+
};
994+
995+
self.eval_fn_call(
996+
fn_val,
997+
(caller_abi, caller_fn_abi),
998+
args,
999+
with_caller_location,
1000+
&destination,
1001+
ret,
1002+
unwind,
1003+
)
1004+
}
1005+
9921006
fn check_fn_target_features(&self, instance: ty::Instance<'tcx>) -> InterpResult<'tcx, ()> {
9931007
// Calling functions with `#[target_feature]` is not unsafe on WASM, see #84988
9941008
let attrs = self.tcx.codegen_fn_attrs(instance.def_id());

0 commit comments

Comments
 (0)