Skip to content

Commit 9ba2d45

Browse files
authored
Rollup merge of rust-lang#150473 - RalfJung:interpret-tail-call, r=WaffleLapkin
tail calls: fix copying non-scalar arguments to callee Alternative to rust-lang#144933: when invoking a tail call with a non-scalar argument, we need to delay freeing the caller's local variables until after the callee is initialized, so that we can copy things from the caller to the callee. Fixes rust-lang#144820... but as the FIXMEs in the code show, it's not clear to me whether these are the right semantics. r? @WaffleLapkin
2 parents 8387095 + ad5108e commit 9ba2d45

File tree

9 files changed

+98
-92
lines changed

9 files changed

+98
-92
lines changed

compiler/rustc_const_eval/src/const_eval/machine.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ impl<'tcx> CompileTimeInterpCx<'tcx> {
236236
if self.tcx.is_lang_item(def_id, LangItem::PanicDisplay)
237237
|| self.tcx.is_lang_item(def_id, LangItem::BeginPanic)
238238
{
239-
let args = self.copy_fn_args(args);
239+
let args = Self::copy_fn_args(args);
240240
// &str or &&str
241241
assert!(args.len() == 1);
242242

compiler/rustc_const_eval/src/interpret/call.rs

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use tracing::{info, instrument, trace};
1919

2020
use super::{
2121
CtfeProvenance, FnVal, ImmTy, InterpCx, InterpResult, MPlaceTy, Machine, OpTy, PlaceTy,
22-
Projectable, Provenance, ReturnAction, ReturnContinuation, Scalar, StackPopInfo, interp_ok,
23-
throw_ub, throw_ub_custom,
22+
Projectable, Provenance, ReturnAction, ReturnContinuation, Scalar, interp_ok, throw_ub,
23+
throw_ub_custom,
2424
};
2525
use crate::enter_trace_span;
2626
use crate::interpret::EnteredTraceSpan;
@@ -43,25 +43,22 @@ impl<'tcx, Prov: Provenance> FnArg<'tcx, Prov> {
4343
FnArg::InPlace(mplace) => &mplace.layout,
4444
}
4545
}
46-
}
4746

48-
impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
4947
/// Make a copy of the given fn_arg. Any `InPlace` are degenerated to copies, no protection of the
5048
/// original memory occurs.
51-
pub fn copy_fn_arg(&self, arg: &FnArg<'tcx, M::Provenance>) -> OpTy<'tcx, M::Provenance> {
52-
match arg {
49+
pub fn copy_fn_arg(&self) -> OpTy<'tcx, Prov> {
50+
match self {
5351
FnArg::Copy(op) => op.clone(),
5452
FnArg::InPlace(mplace) => mplace.clone().into(),
5553
}
5654
}
55+
}
5756

57+
impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
5858
/// Make a copy of the given fn_args. Any `InPlace` are degenerated to copies, no protection of the
5959
/// original memory occurs.
60-
pub fn copy_fn_args(
61-
&self,
62-
args: &[FnArg<'tcx, M::Provenance>],
63-
) -> Vec<OpTy<'tcx, M::Provenance>> {
64-
args.iter().map(|fn_arg| self.copy_fn_arg(fn_arg)).collect()
60+
pub fn copy_fn_args(args: &[FnArg<'tcx, M::Provenance>]) -> Vec<OpTy<'tcx, M::Provenance>> {
61+
args.iter().map(|fn_arg| fn_arg.copy_fn_arg()).collect()
6562
}
6663

6764
/// Helper function for argument untupling.
@@ -319,7 +316,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
319316
// We work with a copy of the argument for now; if this is in-place argument passing, we
320317
// will later protect the source it comes from. This means the callee cannot observe if we
321318
// did in-place of by-copy argument passing, except for pointer equality tests.
322-
let caller_arg_copy = self.copy_fn_arg(caller_arg);
319+
let caller_arg_copy = caller_arg.copy_fn_arg();
323320
if !already_live {
324321
let local = callee_arg.as_local().unwrap();
325322
let meta = caller_arg_copy.meta();
@@ -616,7 +613,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
616613
if let Some(fallback) = M::call_intrinsic(
617614
self,
618615
instance,
619-
&self.copy_fn_args(args),
616+
&Self::copy_fn_args(args),
620617
destination,
621618
target,
622619
unwind,
@@ -703,7 +700,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
703700
// An `InPlace` does nothing here, we keep the original receiver intact. We can't
704701
// really pass the argument in-place anyway, and we are constructing a new
705702
// `Immediate` receiver.
706-
let mut receiver = self.copy_fn_arg(&args[0]);
703+
let mut receiver = args[0].copy_fn_arg();
707704
let receiver_place = loop {
708705
match receiver.layout.ty.kind() {
709706
ty::Ref(..) | ty::RawPtr(..) => {
@@ -824,41 +821,50 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
824821
with_caller_location: bool,
825822
) -> InterpResult<'tcx> {
826823
trace!("init_fn_tail_call: {:#?}", fn_val);
827-
828824
// This is the "canonical" implementation of tails calls,
829825
// a pop of the current stack frame, followed by a normal call
830826
// which pushes a new stack frame, with the return address from
831827
// the popped stack frame.
832828
//
833-
// Note that we are using `pop_stack_frame_raw` and not `return_from_current_stack_frame`,
834-
// as the latter "executes" the goto to the return block, but we don't want to,
829+
// Note that we cannot use `return_from_current_stack_frame`,
830+
// as that "executes" the goto to the return block, but we don't want to,
835831
// only the tail called function should return to the current return block.
836-
let StackPopInfo { return_action, return_cont, return_place } =
837-
self.pop_stack_frame_raw(false, |_this, _return_place| {
838-
// This function's return value is just discarded, the tail-callee will fill in the return place instead.
839-
interp_ok(())
840-
})?;
841832

842-
assert_eq!(return_action, ReturnAction::Normal);
843-
844-
// Take the "stack pop cleanup" info, and use that to initiate the next call.
845-
let ReturnContinuation::Goto { ret, unwind } = return_cont else {
846-
bug!("can't tailcall as root");
833+
// The arguments need to all be copied since the current stack frame will be removed
834+
// before the callee even starts executing.
835+
// FIXME(explicit_tail_calls,#144855): does this match what codegen does?
836+
let args = args.iter().map(|fn_arg| FnArg::Copy(fn_arg.copy_fn_arg())).collect::<Vec<_>>();
837+
// Remove the frame from the stack.
838+
let frame = self.pop_stack_frame_raw()?;
839+
// Remember where this frame would have returned to.
840+
let ReturnContinuation::Goto { ret, unwind } = frame.return_cont() else {
841+
bug!("can't tailcall as root of the stack");
847842
};
848-
843+
// There's no return value to deal with! Instead, we forward the old return place
844+
// to the new function.
849845
// FIXME(explicit_tail_calls):
850846
// we should check if both caller&callee can/n't unwind,
851847
// see <https://github.com/rust-lang/rust/pull/113128#issuecomment-1614979803>
852848

849+
// Now push the new stack frame.
853850
self.init_fn_call(
854851
fn_val,
855852
(caller_abi, caller_fn_abi),
856-
args,
853+
&*args,
857854
with_caller_location,
858-
&return_place,
855+
frame.return_place(),
859856
ret,
860857
unwind,
861-
)
858+
)?;
859+
860+
// Finally, clear the local variables. Has to be done after pushing to support
861+
// non-scalar arguments.
862+
// FIXME(explicit_tail_calls,#144855): revisit this once codegen supports indirect
863+
// arguments, to ensure the semantics are compatible.
864+
let return_action = self.cleanup_stack_frame(/* unwinding */ false, frame)?;
865+
assert_eq!(return_action, ReturnAction::Normal);
866+
867+
interp_ok(())
862868
}
863869

864870
pub(super) fn init_drop_in_place_call(
@@ -953,14 +959,18 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
953959
// local's value out.
954960
let return_op =
955961
self.local_to_op(mir::RETURN_PLACE, None).expect("return place should always be live");
956-
// Do the actual pop + copy.
957-
let stack_pop_info = self.pop_stack_frame_raw(unwinding, |this, return_place| {
958-
this.copy_op_allow_transmute(&return_op, return_place)?;
959-
trace!("return value: {:?}", this.dump_place(return_place));
960-
interp_ok(())
961-
})?;
962-
963-
match stack_pop_info.return_action {
962+
// Remove the frame from the stack.
963+
let frame = self.pop_stack_frame_raw()?;
964+
// Copy the return value and remember the return continuation.
965+
if !unwinding {
966+
self.copy_op_allow_transmute(&return_op, frame.return_place())?;
967+
trace!("return value: {:?}", self.dump_place(frame.return_place()));
968+
}
969+
let return_cont = frame.return_cont();
970+
// Finish popping the stack frame.
971+
let return_action = self.cleanup_stack_frame(unwinding, frame)?;
972+
// Jump to the next block.
973+
match return_action {
964974
ReturnAction::Normal => {}
965975
ReturnAction::NoJump => {
966976
// The hook already did everything.
@@ -978,7 +988,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
978988
// Normal return, figure out where to jump.
979989
if unwinding {
980990
// Follow the unwind edge.
981-
match stack_pop_info.return_cont {
991+
match return_cont {
982992
ReturnContinuation::Goto { unwind, .. } => {
983993
// This must be the very last thing that happens, since it can in fact push a new stack frame.
984994
self.unwind_to_block(unwind)
@@ -989,7 +999,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
989999
}
9901000
} else {
9911001
// Follow the normal return edge.
992-
match stack_pop_info.return_cont {
1002+
match return_cont {
9931003
ReturnContinuation::Goto { ret, .. } => self.return_to_block(ret),
9941004
ReturnContinuation::Stop { .. } => {
9951005
assert!(

compiler/rustc_const_eval/src/interpret/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub use self::operand::{ImmTy, Immediate, OpTy};
3636
pub use self::place::{MPlaceTy, MemPlaceMeta, PlaceTy, Writeable};
3737
use self::place::{MemPlace, Place};
3838
pub use self::projection::{OffsetMode, Projectable};
39-
pub use self::stack::{Frame, FrameInfo, LocalState, ReturnContinuation, StackPopInfo};
39+
pub use self::stack::{Frame, FrameInfo, LocalState, ReturnContinuation};
4040
pub use self::util::EnteredTraceSpan;
4141
pub(crate) use self::util::create_static_alloc;
4242
pub use self::validity::{CtfeValidationMode, RangeSet, RefTracking};

compiler/rustc_const_eval/src/interpret/stack.rs

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub struct Frame<'tcx, Prov: Provenance = CtfeProvenance, Extra = ()> {
8181
/// and its layout in the caller. This place is to be interpreted relative to the
8282
/// *caller's* stack frame. We use a `PlaceTy` instead of an `MPlaceTy` since this
8383
/// avoids having to move *all* return places into Miri's memory.
84-
pub return_place: PlaceTy<'tcx, Prov>,
84+
return_place: PlaceTy<'tcx, Prov>,
8585

8686
/// The list of locals for this stack frame, stored in order as
8787
/// `[return_ptr, arguments..., variables..., temporaries...]`.
@@ -127,19 +127,6 @@ pub enum ReturnContinuation {
127127
Stop { cleanup: bool },
128128
}
129129

130-
/// Return type of [`InterpCx::pop_stack_frame_raw`].
131-
pub struct StackPopInfo<'tcx, Prov: Provenance> {
132-
/// Additional information about the action to be performed when returning from the popped
133-
/// stack frame.
134-
pub return_action: ReturnAction,
135-
136-
/// [`return_cont`](Frame::return_cont) of the popped stack frame.
137-
pub return_cont: ReturnContinuation,
138-
139-
/// [`return_place`](Frame::return_place) of the popped stack frame.
140-
pub return_place: PlaceTy<'tcx, Prov>,
141-
}
142-
143130
/// State of a local variable including a memoized layout
144131
#[derive(Clone)]
145132
pub struct LocalState<'tcx, Prov: Provenance = CtfeProvenance> {
@@ -292,6 +279,14 @@ impl<'tcx, Prov: Provenance, Extra> Frame<'tcx, Prov, Extra> {
292279
self.instance
293280
}
294281

282+
pub fn return_place(&self) -> &PlaceTy<'tcx, Prov> {
283+
&self.return_place
284+
}
285+
286+
pub fn return_cont(&self) -> ReturnContinuation {
287+
self.return_cont
288+
}
289+
295290
/// Return the `SourceInfo` of the current instruction.
296291
pub fn current_source_info(&self) -> Option<&mir::SourceInfo> {
297292
self.loc.left().map(|loc| self.body.source_info(loc))
@@ -417,35 +412,26 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
417412
interp_ok(())
418413
}
419414

420-
/// Low-level helper that pops a stack frame from the stack and returns some information about
421-
/// it.
422-
///
423-
/// This also deallocates locals, if necessary.
424-
/// `copy_ret_val` gets called after the frame has been taken from the stack but before the locals have been deallocated.
425-
///
426-
/// [`M::before_stack_pop`] and [`M::after_stack_pop`] are called by this function
427-
/// automatically.
428-
///
429-
/// The high-level version of this is `return_from_current_stack_frame`.
430-
///
431-
/// [`M::before_stack_pop`]: Machine::before_stack_pop
432-
/// [`M::after_stack_pop`]: Machine::after_stack_pop
415+
/// Low-level helper that pops a stack frame from the stack without any cleanup.
416+
/// This invokes `before_stack_pop`.
417+
/// After calling this function, you need to deal with the return value, and then
418+
/// invoke `cleanup_stack_frame`.
433419
pub(super) fn pop_stack_frame_raw(
434420
&mut self,
435-
unwinding: bool,
436-
copy_ret_val: impl FnOnce(&mut Self, &PlaceTy<'tcx, M::Provenance>) -> InterpResult<'tcx>,
437-
) -> InterpResult<'tcx, StackPopInfo<'tcx, M::Provenance>> {
421+
) -> InterpResult<'tcx, Frame<'tcx, M::Provenance, M::FrameExtra>> {
438422
M::before_stack_pop(self)?;
439423
let frame =
440424
self.stack_mut().pop().expect("tried to pop a stack frame, but there were none");
425+
interp_ok(frame)
426+
}
441427

442-
// Copy return value (unless we are unwinding).
443-
if !unwinding {
444-
copy_ret_val(self, &frame.return_place)?;
445-
}
446-
428+
/// Deallocate local variables in the stack frame, and invoke `after_stack_pop`.
429+
pub(super) fn cleanup_stack_frame(
430+
&mut self,
431+
unwinding: bool,
432+
frame: Frame<'tcx, M::Provenance, M::FrameExtra>,
433+
) -> InterpResult<'tcx, ReturnAction> {
447434
let return_cont = frame.return_cont;
448-
let return_place = frame.return_place.clone();
449435

450436
// Cleanup: deallocate locals.
451437
// Usually we want to clean up (deallocate locals), but in a few rare cases we don't.
@@ -455,7 +441,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
455441
ReturnContinuation::Stop { cleanup, .. } => cleanup,
456442
};
457443

458-
let return_action = if cleanup {
444+
if cleanup {
459445
for local in &frame.locals {
460446
self.deallocate_local(local.value)?;
461447
}
@@ -466,13 +452,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
466452
// Call the machine hook, which determines the next steps.
467453
let return_action = M::after_stack_pop(self, frame, unwinding)?;
468454
assert_ne!(return_action, ReturnAction::NoCleanup);
469-
return_action
455+
interp_ok(return_action)
470456
} else {
471457
// We also skip the machine hook when there's no cleanup. This not a real "pop" anyway.
472-
ReturnAction::NoCleanup
473-
};
474-
475-
interp_ok(StackPopInfo { return_action, return_cont, return_place })
458+
interp_ok(ReturnAction::NoCleanup)
459+
}
476460
}
477461

478462
/// In the current stack frame, mark all locals as live that are not arguments and don't have
@@ -655,7 +639,7 @@ impl<'a, 'tcx: 'a, M: Machine<'tcx>> InterpCx<'tcx, M> {
655639
let (_idx, callee_abi) = callee_abis.next().unwrap();
656640
assert!(self.check_argument_compat(caller_abi, callee_abi)?);
657641
// FIXME: do we have to worry about in-place argument passing?
658-
let op = self.copy_fn_arg(fn_arg);
642+
let op = fn_arg.copy_fn_arg();
659643
let mplace = self.allocate(op.layout, MemoryKind::Stack)?;
660644
self.copy_op(&op, &mplace)?;
661645

src/tools/miri/src/concurrency/thread.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ impl VisitProvenance for Thread<'_> {
343343

344344
impl VisitProvenance for Frame<'_, Provenance, FrameExtra<'_>> {
345345
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
346+
let return_place = self.return_place();
346347
let Frame {
347-
return_place,
348348
locals,
349349
extra,
350350
// There are some private fields we cannot access; they contain no tags.

src/tools/miri/src/diagnostics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ pub fn report_result<'tcx>(
493493
for (i, frame) in ecx.active_thread_stack().iter().enumerate() {
494494
trace!("-------------------");
495495
trace!("Frame {}", i);
496-
trace!(" return: {:?}", frame.return_place);
496+
trace!(" return: {:?}", frame.return_place());
497497
for (i, local) in frame.locals.iter().enumerate() {
498498
trace!(" local {}: {:?}", i, local);
499499
}

src/tools/miri/src/machine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,7 +1235,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
12351235
// to run extra MIR), and Ok(Some(body)) if we found MIR to run for the
12361236
// foreign function
12371237
// Any needed call to `goto_block` will be performed by `emulate_foreign_item`.
1238-
let args = ecx.copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
1238+
let args = MiriInterpCx::copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
12391239
let link_name = Symbol::intern(ecx.tcx.symbol_name(instance).name);
12401240
return ecx.emulate_foreign_item(link_name, abi, &args, dest, ret, unwind);
12411241
}
@@ -1262,7 +1262,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
12621262
ret: Option<mir::BasicBlock>,
12631263
unwind: mir::UnwindAction,
12641264
) -> InterpResult<'tcx> {
1265-
let args = ecx.copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
1265+
let args = MiriInterpCx::copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
12661266
ecx.emulate_dyn_sym(fn_val, abi, &args, dest, ret, unwind)
12671267
}
12681268

src/tools/miri/tests/fail/tail_calls/dangling-local-var.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ LL | let local = 0;
1414
help: ALLOC was deallocated here:
1515
--> tests/fail/tail_calls/dangling-local-var.rs:LL:CC
1616
|
17-
LL | f(std::ptr::null());
18-
| ^^^^^^^^^^^^^^^^^^^
17+
LL | let _val = unsafe { *x };
18+
| ^^^^
1919
= note: stack backtrace:
2020
0: g
2121
at tests/fail/tail_calls/dangling-local-var.rs:LL:CC

src/tools/miri/tests/pass/tail_call.rs renamed to src/tools/miri/tests/pass/function_calls/tail_call.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
fn main() {
55
assert_eq!(factorial(10), 3_628_800);
66
assert_eq!(mutually_recursive_identity(1000), 1000);
7+
non_scalar();
78
}
89

910
fn factorial(n: u32) -> u32 {
@@ -37,3 +38,14 @@ fn mutually_recursive_identity(x: u32) -> u32 {
3738

3839
switch(x, 0)
3940
}
41+
42+
fn non_scalar() {
43+
fn f(x: [usize; 2], i: u32) {
44+
if i == 0 {
45+
return;
46+
}
47+
become f(x, i - 1);
48+
}
49+
50+
f([5, 5], 2);
51+
}

0 commit comments

Comments
 (0)