Auto merge of #113128 - WaffleLapkin:become_trully_unuwuable, r=oli-obk,RalfJung
Support tail calls in mir via `TerminatorKind::TailCall` This is one of the interesting bits in tail call implementation — MIR support. This adds a new `TerminatorKind` which represents a tail call: ```rust TailCall { func: Operand<'tcx>, args: Vec<Operand<'tcx>>, fn_span: Span, }, ``` *Structurally* this is very similar to a normal `Call` but is missing a few fields: - `destination` — tail calls don't write to destination, instead they pass caller's destination to the callee (such that eventual `return` will write to the caller of the function that used tail call) - `target` — similarly to `destination` tail calls pass the caller's return address to the callee, so there is nothing to do - `unwind` — I _think_ this is applicable too, although it's a bit confusing - `call_source` — `become` forbids operators and is not created as a lowering of something else; tail calls always come from HIR (at least for now) It might be helpful to read the interpreter implementation to understand what `TailCall` means exactly, although I've tried documenting it too. ----- There are a few `FIXME`-questions still left, ideally we'd be able to answer them during review ':) ----- r? `@oli-obk` cc `@scottmcm` `@DrMeepster` `@JakobDegen`
This commit is contained in:
commit
9af6fee87d
75 changed files with 2385 additions and 173 deletions
|
@ -2,7 +2,9 @@ use crate::build::scope::BreakableTarget;
|
|||
use crate::build::{BlockAnd, BlockAndExtension, BlockFrame, Builder};
|
||||
use rustc_middle::middle::region;
|
||||
use rustc_middle::mir::*;
|
||||
use rustc_middle::span_bug;
|
||||
use rustc_middle::thir::*;
|
||||
use rustc_span::source_map::Spanned;
|
||||
use tracing::debug;
|
||||
|
||||
impl<'a, 'tcx> Builder<'a, 'tcx> {
|
||||
|
@ -91,9 +93,42 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
|
|||
ExprKind::Return { value } => {
|
||||
this.break_scope(block, value, BreakableTarget::Return, source_info)
|
||||
}
|
||||
// FIXME(explicit_tail_calls): properly lower tail calls here
|
||||
ExprKind::Become { value } => {
|
||||
this.break_scope(block, Some(value), BreakableTarget::Return, source_info)
|
||||
let v = &this.thir[value];
|
||||
let ExprKind::Scope { value, lint_level, region_scope } = v.kind else {
|
||||
span_bug!(v.span, "`thir_check_tail_calls` should have disallowed this {v:?}")
|
||||
};
|
||||
|
||||
let v = &this.thir[value];
|
||||
let ExprKind::Call { ref args, fun, fn_span, .. } = v.kind else {
|
||||
span_bug!(v.span, "`thir_check_tail_calls` should have disallowed this {v:?}")
|
||||
};
|
||||
|
||||
this.in_scope((region_scope, source_info), lint_level, |this| {
|
||||
let fun = unpack!(block = this.as_local_operand(block, fun));
|
||||
let args: Box<[_]> = args
|
||||
.into_iter()
|
||||
.copied()
|
||||
.map(|arg| Spanned {
|
||||
node: unpack!(block = this.as_local_call_operand(block, arg)),
|
||||
span: this.thir.exprs[arg].span,
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.record_operands_moved(&args);
|
||||
|
||||
debug!("expr_into_dest: fn_span={:?}", fn_span);
|
||||
|
||||
unpack!(block = this.break_for_tail_call(block, &args, source_info));
|
||||
|
||||
this.cfg.terminate(
|
||||
block,
|
||||
source_info,
|
||||
TerminatorKind::TailCall { func: fun, args, fn_span },
|
||||
);
|
||||
|
||||
this.cfg.start_new_block().unit()
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
assert!(
|
||||
|
|
|
@ -745,6 +745,91 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
|
|||
self.cfg.terminate(block, source_info, TerminatorKind::UnwindResume);
|
||||
}
|
||||
|
||||
/// Sets up the drops for explict tail calls.
|
||||
///
|
||||
/// Unlike other kinds of early exits, tail calls do not go through the drop tree.
|
||||
/// Instead, all scheduled drops are immediately added to the CFG.
|
||||
pub(crate) fn break_for_tail_call(
|
||||
&mut self,
|
||||
mut block: BasicBlock,
|
||||
args: &[Spanned<Operand<'tcx>>],
|
||||
source_info: SourceInfo,
|
||||
) -> BlockAnd<()> {
|
||||
let arg_drops: Vec<_> = args
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|arg| match &arg.node {
|
||||
Operand::Copy(_) => bug!("copy op in tail call args"),
|
||||
Operand::Move(place) => {
|
||||
let local =
|
||||
place.as_local().unwrap_or_else(|| bug!("projection in tail call args"));
|
||||
|
||||
Some(DropData { source_info, local, kind: DropKind::Value })
|
||||
}
|
||||
Operand::Constant(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut unwind_to = self.diverge_cleanup_target(
|
||||
self.scopes.scopes.iter().rev().nth(1).unwrap().region_scope,
|
||||
DUMMY_SP,
|
||||
);
|
||||
let unwind_drops = &mut self.scopes.unwind_drops;
|
||||
|
||||
// the innermost scope contains only the destructors for the tail call arguments
|
||||
// we only want to drop these in case of a panic, so we skip it
|
||||
for scope in self.scopes.scopes[1..].iter().rev().skip(1) {
|
||||
// FIXME(explicit_tail_calls) code duplication with `build_scope_drops`
|
||||
for drop_data in scope.drops.iter().rev() {
|
||||
let source_info = drop_data.source_info;
|
||||
let local = drop_data.local;
|
||||
|
||||
match drop_data.kind {
|
||||
DropKind::Value => {
|
||||
// `unwind_to` should drop the value that we're about to
|
||||
// schedule. If dropping this value panics, then we continue
|
||||
// with the *next* value on the unwind path.
|
||||
debug_assert_eq!(unwind_drops.drops[unwind_to].data.local, drop_data.local);
|
||||
debug_assert_eq!(unwind_drops.drops[unwind_to].data.kind, drop_data.kind);
|
||||
unwind_to = unwind_drops.drops[unwind_to].next;
|
||||
|
||||
let mut unwind_entry_point = unwind_to;
|
||||
|
||||
// the tail call arguments must be dropped if any of these drops panic
|
||||
for drop in arg_drops.iter().copied() {
|
||||
unwind_entry_point = unwind_drops.add_drop(drop, unwind_entry_point);
|
||||
}
|
||||
|
||||
unwind_drops.add_entry_point(block, unwind_entry_point);
|
||||
|
||||
let next = self.cfg.start_new_block();
|
||||
self.cfg.terminate(
|
||||
block,
|
||||
source_info,
|
||||
TerminatorKind::Drop {
|
||||
place: local.into(),
|
||||
target: next,
|
||||
unwind: UnwindAction::Continue,
|
||||
replace: false,
|
||||
},
|
||||
);
|
||||
block = next;
|
||||
}
|
||||
DropKind::Storage => {
|
||||
// Only temps and vars need their storage dead.
|
||||
assert!(local.index() > self.arg_count);
|
||||
self.cfg.push(
|
||||
block,
|
||||
Statement { source_info, kind: StatementKind::StorageDead(local) },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
block.unit()
|
||||
}
|
||||
|
||||
fn leave_top_scope(&mut self, block: BasicBlock) -> BasicBlock {
|
||||
// If we are emitting a `drop` statement, we need to have the cached
|
||||
// diverge cleanup pads ready in case that drop panics.
|
||||
|
@ -1523,6 +1608,7 @@ impl<'tcx> DropTreeBuilder<'tcx> for Unwind {
|
|||
| TerminatorKind::UnwindResume
|
||||
| TerminatorKind::UnwindTerminate(_)
|
||||
| TerminatorKind::Return
|
||||
| TerminatorKind::TailCall { .. }
|
||||
| TerminatorKind::Unreachable
|
||||
| TerminatorKind::Yield { .. }
|
||||
| TerminatorKind::CoroutineDrop
|
||||
|
|
|
@ -217,12 +217,28 @@ impl<'mir, 'tcx, C: TerminatorClassifier<'tcx>> TriColorVisitor<BasicBlocks<'tcx
|
|||
| TerminatorKind::FalseUnwind { .. }
|
||||
| TerminatorKind::Goto { .. }
|
||||
| TerminatorKind::SwitchInt { .. } => ControlFlow::Continue(()),
|
||||
|
||||
// Note that tail call terminator technically returns to the caller,
|
||||
// but for purposes of this lint it makes sense to count it as possibly recursive,
|
||||
// since it's still a call.
|
||||
//
|
||||
// If this'll be repurposed for something else, this might need to be changed.
|
||||
TerminatorKind::TailCall { .. } => ControlFlow::Continue(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_settled(&mut self, bb: BasicBlock) -> ControlFlow<Self::BreakVal> {
|
||||
// When we examine a node for the last time, remember it if it is a recursive call.
|
||||
let terminator = self.body[bb].terminator();
|
||||
|
||||
// FIXME(explicit_tail_calls): highlight tail calls as "recursive call site"
|
||||
//
|
||||
// We don't want to lint functions that recurse only through tail calls
|
||||
// (such as `fn g() { become () }`), so just adding `| TailCall { ... }`
|
||||
// here won't work.
|
||||
//
|
||||
// But at the same time we would like to highlight both calls in a function like
|
||||
// `fn f() { if false { become f() } else { f() } }`, so we need to figure something out.
|
||||
if self.classifier.is_recursive_terminator(self.tcx, self.body, terminator) {
|
||||
self.reachable_recursive_calls.push(terminator.source_info.span);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue