Skip to content

Commit 0d2a400

Browse files
committed
tail calls: fix copying non-scalar arguments to callee
1 parent 23d01cd commit 0d2a400

File tree

9 files changed

+97
-91
lines changed

9 files changed

+97
-91
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
@@ -235,7 +235,7 @@ impl<'tcx> CompileTimeInterpCx<'tcx> {
235235
if self.tcx.is_lang_item(def_id, LangItem::PanicDisplay)
236236
|| self.tcx.is_lang_item(def_id, LangItem::BeginPanic)
237237
{
238-
let args = self.copy_fn_args(args);
238+
let args = Self::copy_fn_args(args);
239239
// &str or &&str
240240
assert!(args.len() == 1);
241241

compiler/rustc_const_eval/src/interpret/call.rs

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

1717
use super::{
1818
CtfeProvenance, FnVal, ImmTy, InterpCx, InterpResult, MPlaceTy, Machine, OpTy, PlaceTy,
19-
Projectable, Provenance, ReturnAction, ReturnContinuation, Scalar, StackPopInfo, interp_ok,
20-
throw_ub, throw_ub_custom, throw_unsup_format,
19+
Projectable, Provenance, ReturnAction, ReturnContinuation, Scalar, interp_ok, throw_ub,
20+
throw_ub_custom, throw_unsup_format,
2121
};
2222
use crate::interpret::EnteredTraceSpan;
2323
use crate::{enter_trace_span, fluent_generated as fluent};
@@ -40,25 +40,22 @@ impl<'tcx, Prov: Provenance> FnArg<'tcx, Prov> {
4040
FnArg::InPlace(mplace) => &mplace.layout,
4141
}
4242
}
43-
}
4443

45-
impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
4644
/// Make a copy of the given fn_arg. Any `InPlace` are degenerated to copies, no protection of the
4745
/// original memory occurs.
48-
pub fn copy_fn_arg(&self, arg: &FnArg<'tcx, M::Provenance>) -> OpTy<'tcx, M::Provenance> {
49-
match arg {
46+
pub fn copy_fn_arg(&self) -> OpTy<'tcx, Prov> {
47+
match self {
5048
FnArg::Copy(op) => op.clone(),
5149
FnArg::InPlace(mplace) => mplace.clone().into(),
5250
}
5351
}
52+
}
5453

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

6461
/// Helper function for argument untupling.
@@ -310,7 +307,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
310307
// We work with a copy of the argument for now; if this is in-place argument passing, we
311308
// will later protect the source it comes from. This means the callee cannot observe if we
312309
// did in-place of by-copy argument passing, except for pointer equality tests.
313-
let caller_arg_copy = self.copy_fn_arg(caller_arg);
310+
let caller_arg_copy = caller_arg.copy_fn_arg();
314311
if !already_live {
315312
let local = callee_arg.as_local().unwrap();
316313
let meta = caller_arg_copy.meta();
@@ -559,7 +556,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
559556
if let Some(fallback) = M::call_intrinsic(
560557
self,
561558
instance,
562-
&self.copy_fn_args(args),
559+
&Self::copy_fn_args(args),
563560
destination,
564561
target,
565562
unwind,
@@ -646,7 +643,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
646643
// An `InPlace` does nothing here, we keep the original receiver intact. We can't
647644
// really pass the argument in-place anyway, and we are constructing a new
648645
// `Immediate` receiver.
649-
let mut receiver = self.copy_fn_arg(&args[0]);
646+
let mut receiver = args[0].copy_fn_arg();
650647
let receiver_place = loop {
651648
match receiver.layout.ty.kind() {
652649
ty::Ref(..) | ty::RawPtr(..) => {
@@ -765,41 +762,50 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
765762
with_caller_location: bool,
766763
) -> InterpResult<'tcx> {
767764
trace!("init_fn_tail_call: {:#?}", fn_val);
768-
769765
// This is the "canonical" implementation of tails calls,
770766
// a pop of the current stack frame, followed by a normal call
771767
// which pushes a new stack frame, with the return address from
772768
// the popped stack frame.
773769
//
774-
// Note that we are using `pop_stack_frame_raw` and not `return_from_current_stack_frame`,
775-
// as the latter "executes" the goto to the return block, but we don't want to,
770+
// Note that we cannot use `return_from_current_stack_frame`,
771+
// as that "executes" the goto to the return block, but we don't want to,
776772
// only the tail called function should return to the current return block.
777-
let StackPopInfo { return_action, return_cont, return_place } =
778-
self.pop_stack_frame_raw(false, |_this, _return_place| {
779-
// This function's return value is just discarded, the tail-callee will fill in the return place instead.
780-
interp_ok(())
781-
})?;
782773

783-
assert_eq!(return_action, ReturnAction::Normal);
784-
785-
// Take the "stack pop cleanup" info, and use that to initiate the next call.
786-
let ReturnContinuation::Goto { ret, unwind } = return_cont else {
787-
bug!("can't tailcall as root");
774+
// The arguments need to all be copied since the current stack frame will be removed
775+
// before the callee even starts executing.
776+
// FIXME(explicit_tail_calls): does this match what codegen does?
777+
let args = args.iter().map(|fn_arg| FnArg::Copy(fn_arg.copy_fn_arg())).collect::<Vec<_>>();
778+
// Remove the frame from the stack.
779+
let frame = self.pop_stack_frame_raw()?;
780+
// Remember where this frame would have returned to.
781+
let ReturnContinuation::Goto { ret, unwind } = frame.return_cont() else {
782+
bug!("can't tailcall as root of the stack");
788783
};
789-
784+
// There's no return value to deal with! Instead, we forward the old return place
785+
// to the new function.
790786
// FIXME(explicit_tail_calls):
791787
// we should check if both caller&callee can/n't unwind,
792788
// see <https://github.com/rust-lang/rust/pull/113128#issuecomment-1614979803>
793789

790+
// Now push the new stack frame.
794791
self.init_fn_call(
795792
fn_val,
796793
(caller_abi, caller_fn_abi),
797-
args,
794+
&*args,
798795
with_caller_location,
799-
&return_place,
796+
frame.return_place(),
800797
ret,
801798
unwind,
802-
)
799+
)?;
800+
801+
// Finally, clear the local variables. Has to be done after pushing to support
802+
// non-scalar arguments.
803+
// FIXME(explicit_tail_calls): revisit this once codegen supports indirect arguments,
804+
// to ensure the semantics are compatible.
805+
let return_action = self.cleanup_stack_frame(/* unwinding */ false, frame)?;
806+
assert_eq!(return_action, ReturnAction::Normal);
807+
808+
interp_ok(())
803809
}
804810

805811
pub(super) fn init_drop_in_place_call(
@@ -894,14 +900,18 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
894900
// local's value out.
895901
let return_op =
896902
self.local_to_op(mir::RETURN_PLACE, None).expect("return place should always be live");
897-
// Do the actual pop + copy.
898-
let stack_pop_info = self.pop_stack_frame_raw(unwinding, |this, return_place| {
899-
this.copy_op_allow_transmute(&return_op, return_place)?;
900-
trace!("return value: {:?}", this.dump_place(return_place));
901-
interp_ok(())
902-
})?;
903-
904-
match stack_pop_info.return_action {
903+
// Remove the frame from the stack.
904+
let frame = self.pop_stack_frame_raw()?;
905+
// Copy the return value and remember the return continuation.
906+
if !unwinding {
907+
self.copy_op_allow_transmute(&return_op, frame.return_place())?;
908+
trace!("return value: {:?}", self.dump_place(frame.return_place()));
909+
}
910+
let return_cont = frame.return_cont();
911+
// Finish popping the stack frame.
912+
let return_action = self.cleanup_stack_frame(unwinding, frame)?;
913+
// Jump to the next block.
914+
match return_action {
905915
ReturnAction::Normal => {}
906916
ReturnAction::NoJump => {
907917
// The hook already did everything.
@@ -919,7 +929,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
919929
// Normal return, figure out where to jump.
920930
if unwinding {
921931
// Follow the unwind edge.
922-
match stack_pop_info.return_cont {
932+
match return_cont {
923933
ReturnContinuation::Goto { unwind, .. } => {
924934
// This must be the very last thing that happens, since it can in fact push a new stack frame.
925935
self.unwind_to_block(unwind)
@@ -930,7 +940,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
930940
}
931941
} else {
932942
// Follow the normal return edge.
933-
match stack_pop_info.return_cont {
943+
match return_cont {
934944
ReturnContinuation::Goto { ret, .. } => self.return_to_block(ret),
935945
ReturnContinuation::Stop { .. } => {
936946
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: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub struct Frame<'tcx, Prov: Provenance = CtfeProvenance, Extra = ()> {
8080
/// and its layout in the caller. This place is to be interpreted relative to the
8181
/// *caller's* stack frame. We use a `PlaceTy` instead of an `MPlaceTy` since this
8282
/// avoids having to move *all* return places into Miri's memory.
83-
pub return_place: PlaceTy<'tcx, Prov>,
83+
return_place: PlaceTy<'tcx, Prov>,
8484

8585
/// The list of locals for this stack frame, stored in order as
8686
/// `[return_ptr, arguments..., variables..., temporaries...]`.
@@ -122,19 +122,6 @@ pub enum ReturnContinuation {
122122
Stop { cleanup: bool },
123123
}
124124

125-
/// Return type of [`InterpCx::pop_stack_frame_raw`].
126-
pub struct StackPopInfo<'tcx, Prov: Provenance> {
127-
/// Additional information about the action to be performed when returning from the popped
128-
/// stack frame.
129-
pub return_action: ReturnAction,
130-
131-
/// [`return_cont`](Frame::return_cont) of the popped stack frame.
132-
pub return_cont: ReturnContinuation,
133-
134-
/// [`return_place`](Frame::return_place) of the popped stack frame.
135-
pub return_place: PlaceTy<'tcx, Prov>,
136-
}
137-
138125
/// State of a local variable including a memoized layout
139126
#[derive(Clone)]
140127
pub struct LocalState<'tcx, Prov: Provenance = CtfeProvenance> {
@@ -286,6 +273,14 @@ impl<'tcx, Prov: Provenance, Extra> Frame<'tcx, Prov, Extra> {
286273
self.instance
287274
}
288275

276+
pub fn return_place(&self) -> &PlaceTy<'tcx, Prov> {
277+
&self.return_place
278+
}
279+
280+
pub fn return_cont(&self) -> ReturnContinuation {
281+
self.return_cont
282+
}
283+
289284
/// Return the `SourceInfo` of the current instruction.
290285
pub fn current_source_info(&self) -> Option<&mir::SourceInfo> {
291286
self.loc.left().map(|loc| self.body.source_info(loc))
@@ -410,35 +405,26 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
410405
interp_ok(())
411406
}
412407

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

435-
// Copy return value (unless we are unwinding).
436-
if !unwinding {
437-
copy_ret_val(self, &frame.return_place)?;
438-
}
439-
421+
/// Deallocate local variables in the stack frame, and invoke `after_stack_pop`.
422+
pub(super) fn cleanup_stack_frame(
423+
&mut self,
424+
unwinding: bool,
425+
frame: Frame<'tcx, M::Provenance, M::FrameExtra>,
426+
) -> InterpResult<'tcx, ReturnAction> {
440427
let return_cont = frame.return_cont;
441-
let return_place = frame.return_place.clone();
442428

443429
// Cleanup: deallocate locals.
444430
// Usually we want to clean up (deallocate locals), but in a few rare cases we don't.
@@ -448,7 +434,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
448434
ReturnContinuation::Stop { cleanup, .. } => cleanup,
449435
};
450436

451-
let return_action = if cleanup {
437+
if cleanup {
452438
// We need to take the locals out, since we need to mutate while iterating.
453439
for local in &frame.locals {
454440
self.deallocate_local(local.value)?;
@@ -457,13 +443,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
457443
// Call the machine hook, which determines the next steps.
458444
let return_action = M::after_stack_pop(self, frame, unwinding)?;
459445
assert_ne!(return_action, ReturnAction::NoCleanup);
460-
return_action
446+
interp_ok(return_action)
461447
} else {
462448
// We also skip the machine hook when there's no cleanup. This not a real "pop" anyway.
463-
ReturnAction::NoCleanup
464-
};
465-
466-
interp_ok(StackPopInfo { return_action, return_cont, return_place })
449+
interp_ok(ReturnAction::NoCleanup)
450+
}
467451
}
468452

469453
/// In the current stack frame, mark all locals as live that are not arguments and don't have

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
@@ -497,7 +497,7 @@ pub fn report_result<'tcx>(
497497
for (i, frame) in ecx.active_thread_stack().iter().enumerate() {
498498
trace!("-------------------");
499499
trace!("Frame {}", i);
500-
trace!(" return: {:?}", frame.return_place);
500+
trace!(" return: {:?}", frame.return_place());
501501
for (i, local) in frame.locals.iter().enumerate() {
502502
trace!(" local {}: {:?}", i, local);
503503
}

src/tools/miri/src/machine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,7 +1228,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
12281228
// to run extra MIR), and Ok(Some(body)) if we found MIR to run for the
12291229
// foreign function
12301230
// Any needed call to `goto_block` will be performed by `emulate_foreign_item`.
1231-
let args = ecx.copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
1231+
let args = MiriInterpCx::copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
12321232
let link_name = Symbol::intern(ecx.tcx.symbol_name(instance).name);
12331233
return ecx.emulate_foreign_item(link_name, abi, &args, dest, ret, unwind);
12341234
}
@@ -1255,7 +1255,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
12551255
ret: Option<mir::BasicBlock>,
12561256
unwind: mir::UnwindAction,
12571257
) -> InterpResult<'tcx> {
1258-
let args = ecx.copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
1258+
let args = MiriInterpCx::copy_fn_args(args); // FIXME: Should `InPlace` arguments be reset to uninit?
12591259
ecx.emulate_dyn_sym(fn_val, abi, &args, dest, ret, unwind)
12601260
}
12611261

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: BACKTRACE (of the first span):
2020
= note: inside `g` at tests/fail/tail_calls/dangling-local-var.rs:LL:CC
2121
note: inside `main`

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)