1
Fork 0

Auto merge of #121801 - zetanumbers:async_drop_glue, r=oli-obk

Add simple async drop glue generation

This is a prototype of the async drop glue generation for some simple types. Async drop glue is intended to behave very similar to the regular drop glue except for being asynchronous. Currently it does not execute synchronous drops but only calls user implementations of `AsyncDrop::async_drop` associative function and awaits the returned future. It is not complete as it only recurses into arrays, slices, tuples, and structs and does not have same sensible restrictions as the old `Drop` trait implementation like having the same bounds as the type definition, while code assumes their existence (requires a future work).

This current design uses a workaround as it does not create any custom async destructor state machine types for ADTs, but instead uses types defined in the std library called future combinators (deferred_async_drop, chain, ready_unit).

Also I recommend reading my [explainer](https://zetanumbers.github.io/book/async-drop-design.html).

This is a part of the [MCP: Low level components for async drop](https://github.com/rust-lang/compiler-team/issues/727) work.

Feature completeness:

 - [x] `AsyncDrop` trait
 - [ ] `async_drop_in_place_raw`/async drop glue generation support for
   - [x] Trivially destructible types (integers, bools, floats, string slices, pointers, references, etc.)
   - [x] Arrays and slices (array pointer is unsized into slice pointer)
   - [x] ADTs (enums, structs, unions)
   - [x] tuple-like types (tuples, closures)
   - [ ] Dynamic types (`dyn Trait`, see explainer's [proposed design](https://github.com/zetanumbers/posts/blob/main/async-drop-design.md#async-drop-glue-for-dyn-trait))
   - [ ] coroutines (https://github.com/rust-lang/rust/pull/123948)
 - [x] Async drop glue includes sync drop glue code
 - [x] Cleanup branch generation for `async_drop_in_place_raw`
 - [ ] Union rejects non-trivially async destructible fields
 - [ ] `AsyncDrop` implementation requires same bounds as type definition
 - [ ] Skip trivially destructible fields (optimization)
 - [ ] New [`TyKind::AdtAsyncDestructor`](https://github.com/zetanumbers/posts/blob/main/async-drop-design.md#adt-async-destructor-types) and get rid of combinators
 - [ ] [Synchronously undroppable types](https://github.com/zetanumbers/posts/blob/main/async-drop-design.md#exclusively-async-drop)
 - [ ] Automatic async drop at the end of the scope in async context
This commit is contained in:
bors 2024-04-23 02:10:23 +00:00
commit aca749eefc
44 changed files with 1916 additions and 24 deletions

View file

@ -332,7 +332,8 @@ impl<'tcx> Inliner<'tcx> {
| InstanceDef::DropGlue(..)
| InstanceDef::CloneShim(..)
| InstanceDef::ThreadLocalShim(..)
| InstanceDef::FnPtrAddrShim(..) => return Ok(()),
| InstanceDef::FnPtrAddrShim(..)
| InstanceDef::AsyncDropGlueCtorShim(..) => return Ok(()),
}
if self.tcx.is_constructor(callee_def_id) {
@ -1083,7 +1084,8 @@ fn try_instance_mir<'tcx>(
tcx: TyCtxt<'tcx>,
instance: InstanceDef<'tcx>,
) -> Result<&'tcx Body<'tcx>, &'static str> {
if let ty::InstanceDef::DropGlue(_, Some(ty)) = instance
if let ty::InstanceDef::DropGlue(_, Some(ty))
| ty::InstanceDef::AsyncDropGlueCtorShim(_, Some(ty)) = instance
&& let ty::Adt(def, args) = ty.kind()
{
let fields = def.all_fields();

View file

@ -94,8 +94,10 @@ pub(crate) fn mir_callgraph_reachable<'tcx>(
| InstanceDef::CloneShim(..) => {}
// This shim does not call any other functions, thus there can be no recursion.
InstanceDef::FnPtrAddrShim(..) => continue,
InstanceDef::DropGlue(..) => {
InstanceDef::FnPtrAddrShim(..) => {
continue;
}
InstanceDef::DropGlue(..) | InstanceDef::AsyncDropGlueCtorShim(..) => {
// FIXME: A not fully instantiated drop shim can cause ICEs if one attempts to
// have its MIR built. Likely oli-obk just screwed up the `ParamEnv`s, so this
// needs some more analysis.

View file

@ -22,6 +22,8 @@ use crate::{
use rustc_middle::mir::patch::MirPatch;
use rustc_mir_dataflow::elaborate_drops::{self, DropElaborator, DropFlagMode, DropStyle};
mod async_destructor_ctor;
pub fn provide(providers: &mut Providers) {
providers.mir_shims = make_shim;
}
@ -127,6 +129,9 @@ fn make_shim<'tcx>(tcx: TyCtxt<'tcx>, instance: ty::InstanceDef<'tcx>) -> Body<'
ty::InstanceDef::ThreadLocalShim(..) => build_thread_local_shim(tcx, instance),
ty::InstanceDef::CloneShim(def_id, ty) => build_clone_shim(tcx, def_id, ty),
ty::InstanceDef::FnPtrAddrShim(def_id, ty) => build_fn_ptr_addr_shim(tcx, def_id, ty),
ty::InstanceDef::AsyncDropGlueCtorShim(def_id, ty) => {
async_destructor_ctor::build_async_destructor_ctor_shim(tcx, def_id, ty)
}
ty::InstanceDef::Virtual(..) => {
bug!("InstanceDef::Virtual ({:?}) is for direct calls only", instance)
}

View file

@ -0,0 +1,618 @@
use std::iter;
use itertools::Itertools;
use rustc_ast::Mutability;
use rustc_const_eval::interpret;
use rustc_hir::def_id::DefId;
use rustc_hir::lang_items::LangItem;
use rustc_index::{Idx, IndexVec};
use rustc_middle::mir::{
BasicBlock, BasicBlockData, Body, CallSource, CastKind, Const, ConstOperand, ConstValue, Local,
LocalDecl, MirSource, Operand, Place, PlaceElem, Rvalue, SourceInfo, Statement, StatementKind,
Terminator, TerminatorKind, UnwindAction, UnwindTerminateReason, RETURN_PLACE,
};
use rustc_middle::ty::adjustment::PointerCoercion;
use rustc_middle::ty::util::Discr;
use rustc_middle::ty::{self, Ty, TyCtxt};
use rustc_span::source_map::respan;
use rustc_span::{Span, Symbol};
use rustc_target::abi::{FieldIdx, VariantIdx};
use rustc_target::spec::PanicStrategy;
use super::{local_decls_for_sig, new_body};
pub fn build_async_destructor_ctor_shim<'tcx>(
tcx: TyCtxt<'tcx>,
def_id: DefId,
ty: Option<Ty<'tcx>>,
) -> Body<'tcx> {
debug!("build_drop_shim(def_id={:?}, ty={:?})", def_id, ty);
AsyncDestructorCtorShimBuilder::new(tcx, def_id, ty).build()
}
/// Builder for async_drop_in_place shim. Functions as a stack machine
/// to build up an expression using combinators. Stack contains pairs
/// of locals and types. Combinator is a not yet instantiated pair of a
/// function and a type, is considered to be an operator which consumes
/// operands from the stack by instantiating its function and its type
/// with operand types and moving locals into the function call. Top
/// pair is considered to be the last operand.
// FIXME: add mir-opt tests
struct AsyncDestructorCtorShimBuilder<'tcx> {
tcx: TyCtxt<'tcx>,
def_id: DefId,
self_ty: Option<Ty<'tcx>>,
span: Span,
source_info: SourceInfo,
param_env: ty::ParamEnv<'tcx>,
stack: Vec<Operand<'tcx>>,
last_bb: BasicBlock,
top_cleanup_bb: Option<BasicBlock>,
locals: IndexVec<Local, LocalDecl<'tcx>>,
bbs: IndexVec<BasicBlock, BasicBlockData<'tcx>>,
}
#[derive(Clone, Copy)]
enum SurfaceDropKind {
Async,
Sync,
}
impl<'tcx> AsyncDestructorCtorShimBuilder<'tcx> {
const SELF_PTR: Local = Local::from_u32(1);
const INPUT_COUNT: usize = 1;
const MAX_STACK_LEN: usize = 2;
fn new(tcx: TyCtxt<'tcx>, def_id: DefId, self_ty: Option<Ty<'tcx>>) -> Self {
let args = if let Some(ty) = self_ty {
tcx.mk_args(&[ty.into()])
} else {
ty::GenericArgs::identity_for_item(tcx, def_id)
};
let sig = tcx.fn_sig(def_id).instantiate(tcx, args);
let sig = tcx.instantiate_bound_regions_with_erased(sig);
let span = tcx.def_span(def_id);
let source_info = SourceInfo::outermost(span);
debug_assert_eq!(sig.inputs().len(), Self::INPUT_COUNT);
let locals = local_decls_for_sig(&sig, span);
// Usual case: noop() + unwind resume + return
let mut bbs = IndexVec::with_capacity(3);
let param_env = tcx.param_env_reveal_all_normalized(def_id);
AsyncDestructorCtorShimBuilder {
tcx,
def_id,
self_ty,
span,
source_info,
param_env,
stack: Vec::with_capacity(Self::MAX_STACK_LEN),
last_bb: bbs.push(BasicBlockData::new(None)),
top_cleanup_bb: match tcx.sess.panic_strategy() {
PanicStrategy::Unwind => {
// Don't drop input arg because it's just a pointer
Some(bbs.push(BasicBlockData {
statements: Vec::new(),
terminator: Some(Terminator {
source_info,
kind: TerminatorKind::UnwindResume,
}),
is_cleanup: true,
}))
}
PanicStrategy::Abort => None,
},
locals,
bbs,
}
}
fn build(self) -> Body<'tcx> {
let (tcx, def_id, Some(self_ty)) = (self.tcx, self.def_id, self.self_ty) else {
return self.build_zst_output();
};
let surface_drop_kind = || {
let param_env = tcx.param_env_reveal_all_normalized(def_id);
if self_ty.has_surface_async_drop(tcx, param_env) {
Some(SurfaceDropKind::Async)
} else if self_ty.has_surface_drop(tcx, param_env) {
Some(SurfaceDropKind::Sync)
} else {
None
}
};
match self_ty.kind() {
ty::Array(elem_ty, _) => self.build_slice(true, *elem_ty),
ty::Slice(elem_ty) => self.build_slice(false, *elem_ty),
ty::Tuple(elem_tys) => self.build_chain(None, elem_tys.iter()),
ty::Adt(adt_def, args) if adt_def.is_struct() => {
let field_tys = adt_def.non_enum_variant().fields.iter().map(|f| f.ty(tcx, args));
self.build_chain(surface_drop_kind(), field_tys)
}
ty::Closure(_, args) => self.build_chain(None, args.as_closure().upvar_tys().iter()),
ty::CoroutineClosure(_, args) => {
self.build_chain(None, args.as_coroutine_closure().upvar_tys().iter())
}
ty::Adt(adt_def, args) if adt_def.is_enum() => {
self.build_enum(*adt_def, *args, surface_drop_kind())
}
ty::Adt(adt_def, _) => {
assert!(adt_def.is_union());
match surface_drop_kind().unwrap() {
SurfaceDropKind::Async => self.build_fused_async_surface(),
SurfaceDropKind::Sync => self.build_fused_sync_surface(),
}
}
ty::Bound(..)
| ty::Foreign(_)
| ty::Placeholder(_)
| ty::Infer(ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_) | ty::TyVar(_))
| ty::Param(_)
| ty::Alias(..) => {
bug!("Building async destructor for unexpected type: {self_ty:?}")
}
_ => {
bug!(
"Building async destructor constructor shim is not yet implemented for type: {self_ty:?}"
)
}
}
}
fn build_enum(
mut self,
adt_def: ty::AdtDef<'tcx>,
args: ty::GenericArgsRef<'tcx>,
surface_drop: Option<SurfaceDropKind>,
) -> Body<'tcx> {
let tcx = self.tcx;
let surface = match surface_drop {
None => None,
Some(kind) => {
self.put_self();
Some(match kind {
SurfaceDropKind::Async => self.combine_async_surface(),
SurfaceDropKind::Sync => self.combine_sync_surface(),
})
}
};
let mut other = None;
for (variant_idx, discr) in adt_def.discriminants(tcx) {
let variant = adt_def.variant(variant_idx);
let mut chain = None;
for (field_idx, field) in variant.fields.iter_enumerated() {
let field_ty = field.ty(tcx, args);
self.put_variant_field(variant.name, variant_idx, field_idx, field_ty);
let defer = self.combine_defer(field_ty);
chain = Some(match chain {
None => defer,
Some(chain) => self.combine_chain(chain, defer),
})
}
let variant_dtor = chain.unwrap_or_else(|| self.put_noop());
other = Some(match other {
None => variant_dtor,
Some(other) => {
self.put_self();
self.put_discr(discr);
self.combine_either(other, variant_dtor)
}
});
}
let variants_dtor = other.unwrap_or_else(|| self.put_noop());
let dtor = match surface {
None => variants_dtor,
Some(surface) => self.combine_chain(surface, variants_dtor),
};
self.combine_fuse(dtor);
self.return_()
}
fn build_chain<I>(mut self, surface_drop: Option<SurfaceDropKind>, elem_tys: I) -> Body<'tcx>
where
I: Iterator<Item = Ty<'tcx>> + ExactSizeIterator,
{
let surface = match surface_drop {
None => None,
Some(kind) => {
self.put_self();
Some(match kind {
SurfaceDropKind::Async => self.combine_async_surface(),
SurfaceDropKind::Sync => self.combine_sync_surface(),
})
}
};
let mut chain = None;
for (field_idx, field_ty) in elem_tys.enumerate().map(|(i, ty)| (FieldIdx::new(i), ty)) {
self.put_field(field_idx, field_ty);
let defer = self.combine_defer(field_ty);
chain = Some(match chain {
None => defer,
Some(chain) => self.combine_chain(chain, defer),
})
}
let chain = chain.unwrap_or_else(|| self.put_noop());
let dtor = match surface {
None => chain,
Some(surface) => self.combine_chain(surface, chain),
};
self.combine_fuse(dtor);
self.return_()
}
fn build_zst_output(mut self) -> Body<'tcx> {
self.put_zst_output();
self.return_()
}
fn build_fused_async_surface(mut self) -> Body<'tcx> {
self.put_self();
let surface = self.combine_async_surface();
self.combine_fuse(surface);
self.return_()
}
fn build_fused_sync_surface(mut self) -> Body<'tcx> {
self.put_self();
let surface = self.combine_sync_surface();
self.combine_fuse(surface);
self.return_()
}
fn build_slice(mut self, is_array: bool, elem_ty: Ty<'tcx>) -> Body<'tcx> {
if is_array {
self.put_array_as_slice(elem_ty)
} else {
self.put_self()
}
let dtor = self.combine_slice(elem_ty);
self.combine_fuse(dtor);
self.return_()
}
fn put_zst_output(&mut self) {
let return_ty = self.locals[RETURN_PLACE].ty;
self.put_operand(Operand::Constant(Box::new(ConstOperand {
span: self.span,
user_ty: None,
const_: Const::zero_sized(return_ty),
})));
}
/// Puts `to_drop: *mut Self` on top of the stack.
fn put_self(&mut self) {
self.put_operand(Operand::Copy(Self::SELF_PTR.into()))
}
/// Given that `Self is [ElemTy; N]` puts `to_drop: *mut [ElemTy]`
/// on top of the stack.
fn put_array_as_slice(&mut self, elem_ty: Ty<'tcx>) {
let slice_ptr_ty = Ty::new_mut_ptr(self.tcx, Ty::new_slice(self.tcx, elem_ty));
self.put_temp_rvalue(Rvalue::Cast(
CastKind::PointerCoercion(PointerCoercion::Unsize),
Operand::Copy(Self::SELF_PTR.into()),
slice_ptr_ty,
))
}
/// If given Self is a struct puts `to_drop: *mut FieldTy` on top
/// of the stack.
fn put_field(&mut self, field: FieldIdx, field_ty: Ty<'tcx>) {
let place = Place {
local: Self::SELF_PTR,
projection: self
.tcx
.mk_place_elems(&[PlaceElem::Deref, PlaceElem::Field(field, field_ty)]),
};
self.put_temp_rvalue(Rvalue::AddressOf(Mutability::Mut, place))
}
/// If given Self is an enum puts `to_drop: *mut FieldTy` on top of
/// the stack.
fn put_variant_field(
&mut self,
variant_sym: Symbol,
variant: VariantIdx,
field: FieldIdx,
field_ty: Ty<'tcx>,
) {
let place = Place {
local: Self::SELF_PTR,
projection: self.tcx.mk_place_elems(&[
PlaceElem::Deref,
PlaceElem::Downcast(Some(variant_sym), variant),
PlaceElem::Field(field, field_ty),
]),
};
self.put_temp_rvalue(Rvalue::AddressOf(Mutability::Mut, place))
}
/// If given Self is an enum puts `to_drop: *mut FieldTy` on top of
/// the stack.
fn put_discr(&mut self, discr: Discr<'tcx>) {
let (size, _) = discr.ty.int_size_and_signed(self.tcx);
self.put_operand(Operand::const_from_scalar(
self.tcx,
discr.ty,
interpret::Scalar::from_uint(discr.val, size),
self.span,
));
}
/// Puts `x: RvalueType` on top of the stack.
fn put_temp_rvalue(&mut self, rvalue: Rvalue<'tcx>) {
let last_bb = &mut self.bbs[self.last_bb];
debug_assert!(last_bb.terminator.is_none());
let source_info = self.source_info;
let local_ty = rvalue.ty(&self.locals, self.tcx);
// We need to create a new local to be able to "consume" it with
// a combinator
let local = self.locals.push(LocalDecl::with_source_info(local_ty, source_info));
last_bb.statements.extend_from_slice(&[
Statement { source_info, kind: StatementKind::StorageLive(local) },
Statement {
source_info,
kind: StatementKind::Assign(Box::new((local.into(), rvalue))),
},
]);
self.put_operand(Operand::Move(local.into()));
}
/// Puts operand on top of the stack.
fn put_operand(&mut self, operand: Operand<'tcx>) {
if let Some(top_cleanup_bb) = &mut self.top_cleanup_bb {
let source_info = self.source_info;
match &operand {
Operand::Copy(_) | Operand::Constant(_) => {
*top_cleanup_bb = self.bbs.push(BasicBlockData {
statements: Vec::new(),
terminator: Some(Terminator {
source_info,
kind: TerminatorKind::Goto { target: *top_cleanup_bb },
}),
is_cleanup: true,
});
}
Operand::Move(place) => {
let local = place.as_local().unwrap();
*top_cleanup_bb = self.bbs.push(BasicBlockData {
statements: Vec::new(),
terminator: Some(Terminator {
source_info,
kind: if self.locals[local].ty.needs_drop(self.tcx, self.param_env) {
TerminatorKind::Drop {
place: local.into(),
target: *top_cleanup_bb,
unwind: UnwindAction::Terminate(
UnwindTerminateReason::InCleanup,
),
replace: false,
}
} else {
TerminatorKind::Goto { target: *top_cleanup_bb }
},
}),
is_cleanup: true,
});
}
};
}
self.stack.push(operand);
}
/// Puts `noop: async_drop::Noop` on top of the stack
fn put_noop(&mut self) -> Ty<'tcx> {
self.apply_combinator(0, LangItem::AsyncDropNoop, &[])
}
fn combine_async_surface(&mut self) -> Ty<'tcx> {
self.apply_combinator(1, LangItem::SurfaceAsyncDropInPlace, &[self.self_ty.unwrap().into()])
}
fn combine_sync_surface(&mut self) -> Ty<'tcx> {
self.apply_combinator(
1,
LangItem::AsyncDropSurfaceDropInPlace,
&[self.self_ty.unwrap().into()],
)
}
fn combine_fuse(&mut self, inner_future_ty: Ty<'tcx>) -> Ty<'tcx> {
self.apply_combinator(1, LangItem::AsyncDropFuse, &[inner_future_ty.into()])
}
fn combine_slice(&mut self, elem_ty: Ty<'tcx>) -> Ty<'tcx> {
self.apply_combinator(1, LangItem::AsyncDropSlice, &[elem_ty.into()])
}
fn combine_defer(&mut self, to_drop_ty: Ty<'tcx>) -> Ty<'tcx> {
self.apply_combinator(1, LangItem::AsyncDropDefer, &[to_drop_ty.into()])
}
fn combine_chain(&mut self, first: Ty<'tcx>, second: Ty<'tcx>) -> Ty<'tcx> {
self.apply_combinator(2, LangItem::AsyncDropChain, &[first.into(), second.into()])
}
fn combine_either(&mut self, other: Ty<'tcx>, matched: Ty<'tcx>) -> Ty<'tcx> {
self.apply_combinator(
4,
LangItem::AsyncDropEither,
&[other.into(), matched.into(), self.self_ty.unwrap().into()],
)
}
fn return_(mut self) -> Body<'tcx> {
let last_bb = &mut self.bbs[self.last_bb];
debug_assert!(last_bb.terminator.is_none());
let source_info = self.source_info;
let (1, Some(output)) = (self.stack.len(), self.stack.pop()) else {
span_bug!(
self.span,
"async destructor ctor shim builder finished with invalid number of stack items: expected 1 found {}",
self.stack.len(),
)
};
#[cfg(debug_assertions)]
if let Some(ty) = self.self_ty {
debug_assert_eq!(
output.ty(&self.locals, self.tcx),
ty.async_destructor_ty(self.tcx, self.param_env),
"output async destructor types did not match for type: {ty:?}",
);
}
let dead_storage = match &output {
Operand::Move(place) => Some(Statement {
source_info,
kind: StatementKind::StorageDead(place.as_local().unwrap()),
}),
_ => None,
};
last_bb.statements.extend(
iter::once(Statement {
source_info,
kind: StatementKind::Assign(Box::new((RETURN_PLACE.into(), Rvalue::Use(output)))),
})
.chain(dead_storage),
);
last_bb.terminator = Some(Terminator { source_info, kind: TerminatorKind::Return });
let source = MirSource::from_instance(ty::InstanceDef::AsyncDropGlueCtorShim(
self.def_id,
self.self_ty,
));
new_body(source, self.bbs, self.locals, Self::INPUT_COUNT, self.span)
}
fn apply_combinator(
&mut self,
arity: usize,
function: LangItem,
args: &[ty::GenericArg<'tcx>],
) -> Ty<'tcx> {
let function = self.tcx.require_lang_item(function, Some(self.span));
let operands_split = self
.stack
.len()
.checked_sub(arity)
.expect("async destructor ctor shim combinator tried to consume too many items");
let operands = &self.stack[operands_split..];
let func_ty = Ty::new_fn_def(self.tcx, function, args.iter().copied());
let func_sig = func_ty.fn_sig(self.tcx).no_bound_vars().unwrap();
#[cfg(debug_assertions)]
operands.iter().zip(func_sig.inputs()).for_each(|(operand, expected_ty)| {
let operand_ty = operand.ty(&self.locals, self.tcx);
if operand_ty == *expected_ty {
return;
}
// If projection of Discriminant then compare with `Ty::discriminant_ty`
if let ty::Alias(ty::AliasKind::Projection, ty::AliasTy { args, def_id, .. }) =
expected_ty.kind()
&& Some(*def_id) == self.tcx.lang_items().discriminant_type()
&& args.first().unwrap().as_type().unwrap().discriminant_ty(self.tcx) == operand_ty
{
return;
}
span_bug!(
self.span,
"Operand type and combinator argument type are not equal.
operand_ty: {:?}
argument_ty: {:?}
",
operand_ty,
expected_ty
);
});
let target = self.bbs.push(BasicBlockData {
statements: operands
.iter()
.rev()
.filter_map(|o| {
if let Operand::Move(Place { local, projection }) = o {
assert!(projection.is_empty());
Some(Statement {
source_info: self.source_info,
kind: StatementKind::StorageDead(*local),
})
} else {
None
}
})
.collect(),
terminator: None,
is_cleanup: false,
});
let dest_ty = func_sig.output();
let dest =
self.locals.push(LocalDecl::with_source_info(dest_ty, self.source_info).immutable());
let unwind = if let Some(top_cleanup_bb) = &mut self.top_cleanup_bb {
for _ in 0..arity {
*top_cleanup_bb =
self.bbs[*top_cleanup_bb].terminator().successors().exactly_one().ok().unwrap();
}
UnwindAction::Cleanup(*top_cleanup_bb)
} else {
UnwindAction::Unreachable
};
let last_bb = &mut self.bbs[self.last_bb];
debug_assert!(last_bb.terminator.is_none());
last_bb.statements.push(Statement {
source_info: self.source_info,
kind: StatementKind::StorageLive(dest),
});
last_bb.terminator = Some(Terminator {
source_info: self.source_info,
kind: TerminatorKind::Call {
func: Operand::Constant(Box::new(ConstOperand {
span: self.span,
user_ty: None,
const_: Const::Val(ConstValue::ZeroSized, func_ty),
})),
destination: dest.into(),
target: Some(target),
unwind,
call_source: CallSource::Misc,
fn_span: self.span,
args: self.stack.drain(operands_split..).map(|o| respan(self.span, o)).collect(),
},
});
self.put_operand(Operand::Move(dest.into()));
self.last_bb = target;
dest_ty
}
}