address review comments
This commit is contained in:
parent
24c1a07c72
commit
c6d0b5bdd8
4 changed files with 138 additions and 127 deletions
|
@ -585,7 +585,7 @@ impl<'a, 'gcx, 'tcx> TyCtxt<'a, 'gcx, 'tcx> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn const_usize(&self, val: usize) -> ConstInt {
|
pub fn const_usize(&self, val: u16) -> ConstInt {
|
||||||
match self.sess.target.uint_type {
|
match self.sess.target.uint_type {
|
||||||
ast::UintTy::U16 => ConstInt::Usize(ConstUsize::Us16(val as u16)),
|
ast::UintTy::U16 => ConstInt::Usize(ConstUsize::Us16(val as u16)),
|
||||||
ast::UintTy::U32 => ConstInt::Usize(ConstUsize::Us32(val as u32)),
|
ast::UintTy::U32 => ConstInt::Usize(ConstUsize::Us32(val as u32)),
|
||||||
|
|
|
@ -22,7 +22,7 @@ use rustc::util::nodemap::FxHashMap;
|
||||||
use rustc_data_structures::indexed_set::IdxSetBuf;
|
use rustc_data_structures::indexed_set::IdxSetBuf;
|
||||||
use rustc_data_structures::indexed_vec::Idx;
|
use rustc_data_structures::indexed_vec::Idx;
|
||||||
use rustc_mir::util::patch::MirPatch;
|
use rustc_mir::util::patch::MirPatch;
|
||||||
use rustc_mir::util::elaborate_drops::{DropFlagState, elaborate_drop};
|
use rustc_mir::util::elaborate_drops::{DropFlagState, Unwind, elaborate_drop};
|
||||||
use rustc_mir::util::elaborate_drops::{DropElaborator, DropStyle, DropFlagMode};
|
use rustc_mir::util::elaborate_drops::{DropElaborator, DropStyle, DropFlagMode};
|
||||||
use syntax::ast;
|
use syntax::ast;
|
||||||
use syntax_pos::Span;
|
use syntax_pos::Span;
|
||||||
|
@ -399,14 +399,13 @@ impl<'b, 'tcx> ElaborateDropsCtxt<'b, 'tcx> {
|
||||||
ctxt: self
|
ctxt: self
|
||||||
},
|
},
|
||||||
terminator.source_info,
|
terminator.source_info,
|
||||||
data.is_cleanup,
|
|
||||||
location,
|
location,
|
||||||
path,
|
path,
|
||||||
target,
|
target,
|
||||||
if data.is_cleanup {
|
if data.is_cleanup {
|
||||||
None
|
Unwind::InCleanup
|
||||||
} else {
|
} else {
|
||||||
Some(Option::unwrap_or(unwind, resume_block))
|
Unwind::To(Option::unwrap_or(unwind, resume_block))
|
||||||
},
|
},
|
||||||
bb)
|
bb)
|
||||||
}
|
}
|
||||||
|
@ -455,6 +454,7 @@ impl<'b, 'tcx> ElaborateDropsCtxt<'b, 'tcx> {
|
||||||
let bb = loc.block;
|
let bb = loc.block;
|
||||||
let data = &self.mir[bb];
|
let data = &self.mir[bb];
|
||||||
let terminator = data.terminator();
|
let terminator = data.terminator();
|
||||||
|
assert!(!data.is_cleanup, "DropAndReplace in unwind path not supported");
|
||||||
|
|
||||||
let assign = Statement {
|
let assign = Statement {
|
||||||
kind: StatementKind::Assign(location.clone(), Rvalue::Use(value.clone())),
|
kind: StatementKind::Assign(location.clone(), Rvalue::Use(value.clone())),
|
||||||
|
@ -477,7 +477,7 @@ impl<'b, 'tcx> ElaborateDropsCtxt<'b, 'tcx> {
|
||||||
kind: TerminatorKind::Goto { target: target },
|
kind: TerminatorKind::Goto { target: target },
|
||||||
..*terminator
|
..*terminator
|
||||||
}),
|
}),
|
||||||
is_cleanup: data.is_cleanup,
|
is_cleanup: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
match self.move_data().rev_lookup.find(location) {
|
match self.move_data().rev_lookup.find(location) {
|
||||||
|
@ -491,11 +491,10 @@ impl<'b, 'tcx> ElaborateDropsCtxt<'b, 'tcx> {
|
||||||
ctxt: self
|
ctxt: self
|
||||||
},
|
},
|
||||||
terminator.source_info,
|
terminator.source_info,
|
||||||
data.is_cleanup,
|
|
||||||
location,
|
location,
|
||||||
path,
|
path,
|
||||||
target,
|
target,
|
||||||
Some(unwind),
|
Unwind::To(unwind),
|
||||||
bb);
|
bb);
|
||||||
on_all_children_bits(self.tcx, self.mir, self.move_data(), path, |child| {
|
on_all_children_bits(self.tcx, self.mir, self.move_data(), path, |child| {
|
||||||
self.set_drop_flag(Location { block: target, statement_index: 0 },
|
self.set_drop_flag(Location { block: target, statement_index: 0 },
|
||||||
|
|
|
@ -198,11 +198,10 @@ fn build_drop_shim<'a, 'tcx>(tcx: ty::TyCtxt<'a, 'tcx, 'tcx>,
|
||||||
elaborate_drops::elaborate_drop(
|
elaborate_drops::elaborate_drop(
|
||||||
&mut elaborator,
|
&mut elaborator,
|
||||||
source_info,
|
source_info,
|
||||||
false,
|
|
||||||
&dropee,
|
&dropee,
|
||||||
(),
|
(),
|
||||||
return_block,
|
return_block,
|
||||||
Some(resume_block),
|
elaborate_drops::Unwind::To(resume_block),
|
||||||
START_BLOCK
|
START_BLOCK
|
||||||
);
|
);
|
||||||
elaborator.patch
|
elaborator.patch
|
||||||
|
|
|
@ -50,6 +50,35 @@ pub enum DropFlagMode {
|
||||||
Deep
|
Deep
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum Unwind {
|
||||||
|
To(BasicBlock),
|
||||||
|
InCleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unwind {
|
||||||
|
fn is_cleanup(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Unwind::To(..) => false,
|
||||||
|
Unwind::InCleanup => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_option(self) -> Option<BasicBlock> {
|
||||||
|
match self {
|
||||||
|
Unwind::To(bb) => Some(bb),
|
||||||
|
Unwind::InCleanup => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map<F>(self, f: F) -> Self where F: FnOnce(BasicBlock) -> BasicBlock {
|
||||||
|
match self {
|
||||||
|
Unwind::To(bb) => Unwind::To(f(bb)),
|
||||||
|
Unwind::InCleanup => Unwind::InCleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait DropElaborator<'a, 'tcx: 'a> : fmt::Debug {
|
pub trait DropElaborator<'a, 'tcx: 'a> : fmt::Debug {
|
||||||
type Path : Copy + fmt::Debug;
|
type Path : Copy + fmt::Debug;
|
||||||
|
|
||||||
|
@ -75,28 +104,25 @@ struct DropCtxt<'l, 'b: 'l, 'tcx: 'b, D>
|
||||||
elaborator: &'l mut D,
|
elaborator: &'l mut D,
|
||||||
|
|
||||||
source_info: SourceInfo,
|
source_info: SourceInfo,
|
||||||
is_cleanup: bool,
|
|
||||||
|
|
||||||
lvalue: &'l Lvalue<'tcx>,
|
lvalue: &'l Lvalue<'tcx>,
|
||||||
path: D::Path,
|
path: D::Path,
|
||||||
succ: BasicBlock,
|
succ: BasicBlock,
|
||||||
unwind: Option<BasicBlock>,
|
unwind: Unwind,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elaborate_drop<'b, 'tcx, D>(
|
pub fn elaborate_drop<'b, 'tcx, D>(
|
||||||
elaborator: &mut D,
|
elaborator: &mut D,
|
||||||
source_info: SourceInfo,
|
source_info: SourceInfo,
|
||||||
is_cleanup: bool,
|
|
||||||
lvalue: &Lvalue<'tcx>,
|
lvalue: &Lvalue<'tcx>,
|
||||||
path: D::Path,
|
path: D::Path,
|
||||||
succ: BasicBlock,
|
succ: BasicBlock,
|
||||||
unwind: Option<BasicBlock>,
|
unwind: Unwind,
|
||||||
bb: BasicBlock)
|
bb: BasicBlock)
|
||||||
where D: DropElaborator<'b, 'tcx>
|
where D: DropElaborator<'b, 'tcx>
|
||||||
{
|
{
|
||||||
assert_eq!(unwind.is_none(), is_cleanup);
|
|
||||||
DropCtxt {
|
DropCtxt {
|
||||||
elaborator, source_info, is_cleanup, lvalue, path, succ, unwind
|
elaborator, source_info, lvalue, path, succ, unwind
|
||||||
}.elaborate_drop(bb)
|
}.elaborate_drop(bb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,14 +171,13 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.elaborator.patch().patch_terminator(bb, TerminatorKind::Drop {
|
self.elaborator.patch().patch_terminator(bb, TerminatorKind::Drop {
|
||||||
location: self.lvalue.clone(),
|
location: self.lvalue.clone(),
|
||||||
target: self.succ,
|
target: self.succ,
|
||||||
unwind: self.unwind
|
unwind: self.unwind.into_option(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DropStyle::Conditional => {
|
DropStyle::Conditional => {
|
||||||
let is_cleanup = self.is_cleanup; // FIXME(#6393)
|
let unwind = self.unwind; // FIXME(#6393)
|
||||||
let succ = self.succ;
|
let succ = self.succ;
|
||||||
let drop_bb = self.complete_drop(
|
let drop_bb = self.complete_drop(Some(DropFlagMode::Deep), succ, unwind);
|
||||||
is_cleanup, Some(DropFlagMode::Deep), succ);
|
|
||||||
self.elaborator.patch().patch_terminator(bb, TerminatorKind::Goto {
|
self.elaborator.patch().patch_terminator(bb, TerminatorKind::Goto {
|
||||||
target: drop_bb
|
target: drop_bb
|
||||||
});
|
});
|
||||||
|
@ -189,11 +214,10 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_subpath(&mut self,
|
fn drop_subpath(&mut self,
|
||||||
is_cleanup: bool,
|
|
||||||
lvalue: &Lvalue<'tcx>,
|
lvalue: &Lvalue<'tcx>,
|
||||||
path: Option<D::Path>,
|
path: Option<D::Path>,
|
||||||
succ: BasicBlock,
|
succ: BasicBlock,
|
||||||
unwind: Option<BasicBlock>)
|
unwind: Unwind)
|
||||||
-> BasicBlock
|
-> BasicBlock
|
||||||
{
|
{
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
|
@ -202,7 +226,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
DropCtxt {
|
DropCtxt {
|
||||||
elaborator: self.elaborator,
|
elaborator: self.elaborator,
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
path, lvalue, succ, unwind, is_cleanup
|
path, lvalue, succ, unwind,
|
||||||
}.elaborated_drop_block()
|
}.elaborated_drop_block()
|
||||||
} else {
|
} else {
|
||||||
debug!("drop_subpath: for rest field {:?}", lvalue);
|
debug!("drop_subpath: for rest field {:?}", lvalue);
|
||||||
|
@ -210,11 +234,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
DropCtxt {
|
DropCtxt {
|
||||||
elaborator: self.elaborator,
|
elaborator: self.elaborator,
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
lvalue, succ, unwind, is_cleanup,
|
lvalue, succ, unwind,
|
||||||
// Using `self.path` here to condition the drop on
|
// Using `self.path` here to condition the drop on
|
||||||
// our own drop flag.
|
// our own drop flag.
|
||||||
path: self.path
|
path: self.path
|
||||||
}.complete_drop(is_cleanup, None, succ)
|
}.complete_drop(None, succ, unwind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,24 +246,15 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
/// the list of steps in it in reverse order.
|
/// the list of steps in it in reverse order.
|
||||||
///
|
///
|
||||||
/// `unwind_ladder` is such a list of steps in reverse order,
|
/// `unwind_ladder` is such a list of steps in reverse order,
|
||||||
/// which is called instead of the next step if the drop unwinds
|
/// which is called if the matching step of the drop glue panics.
|
||||||
/// (the first field is never reached). If it is `None`, all
|
fn drop_halfladder(&mut self,
|
||||||
/// unwind targets are left blank.
|
unwind_ladder: &[Unwind],
|
||||||
fn drop_halfladder<'a>(&mut self,
|
succ: BasicBlock,
|
||||||
unwind_ladder: Option<&[BasicBlock]>,
|
fields: &[(Lvalue<'tcx>, Option<D::Path>)])
|
||||||
succ: BasicBlock,
|
-> Vec<BasicBlock>
|
||||||
fields: &[(Lvalue<'tcx>, Option<D::Path>)],
|
|
||||||
is_cleanup: bool)
|
|
||||||
-> Vec<BasicBlock>
|
|
||||||
{
|
{
|
||||||
let mut unwind_succ = if is_cleanup {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
self.unwind
|
|
||||||
};
|
|
||||||
|
|
||||||
let goto = TerminatorKind::Goto { target: succ };
|
let goto = TerminatorKind::Goto { target: succ };
|
||||||
let mut succ = self.new_block(is_cleanup, goto);
|
let mut succ = self.new_block(unwind_ladder[0], goto);
|
||||||
|
|
||||||
// Always clear the "master" drop flag at the bottom of the
|
// Always clear the "master" drop flag at the bottom of the
|
||||||
// ladder. This is needed because the "master" drop flag
|
// ladder. This is needed because the "master" drop flag
|
||||||
|
@ -248,9 +263,8 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
let succ_loc = Location { block: succ, statement_index: 0 };
|
let succ_loc = Location { block: succ, statement_index: 0 };
|
||||||
self.elaborator.clear_drop_flag(succ_loc, self.path, DropFlagMode::Shallow);
|
self.elaborator.clear_drop_flag(succ_loc, self.path, DropFlagMode::Shallow);
|
||||||
|
|
||||||
fields.iter().rev().enumerate().map(|(i, &(ref lv, path))| {
|
fields.iter().rev().zip(unwind_ladder).map(|(&(ref lv, path), &unwind_succ)| {
|
||||||
succ = self.drop_subpath(is_cleanup, lv, path, succ, unwind_succ);
|
succ = self.drop_subpath(lv, path, succ, unwind_succ);
|
||||||
unwind_succ = unwind_ladder.as_ref().map(|p| p[i]);
|
|
||||||
succ
|
succ
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
@ -271,7 +285,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
/// ELAB(drop location.2 [target=`self.unwind`])
|
/// ELAB(drop location.2 [target=`self.unwind`])
|
||||||
fn drop_ladder<'a>(&mut self,
|
fn drop_ladder<'a>(&mut self,
|
||||||
fields: Vec<(Lvalue<'tcx>, Option<D::Path>)>)
|
fields: Vec<(Lvalue<'tcx>, Option<D::Path>)>)
|
||||||
-> (BasicBlock, Option<BasicBlock>)
|
-> (BasicBlock, Unwind)
|
||||||
{
|
{
|
||||||
debug!("drop_ladder({:?}, {:?})", self, fields);
|
debug!("drop_ladder({:?}, {:?})", self, fields);
|
||||||
|
|
||||||
|
@ -282,21 +296,21 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
|
|
||||||
debug!("drop_ladder - fields needing drop: {:?}", fields);
|
debug!("drop_ladder - fields needing drop: {:?}", fields);
|
||||||
|
|
||||||
let unwind_ladder = if self.is_cleanup {
|
let unwind_ladder = vec![Unwind::InCleanup; fields.len() + 1];
|
||||||
None
|
let unwind_ladder: Vec<_> = if let Unwind::To(target) = self.unwind {
|
||||||
|
let halfladder = self.drop_halfladder(&unwind_ladder, target, &fields);
|
||||||
|
Some(self.unwind).into_iter().chain(halfladder.into_iter().map(Unwind::To))
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let unwind = self.unwind.unwrap(); // FIXME(#6393)
|
unwind_ladder
|
||||||
Some(self.drop_halfladder(None, unwind, &fields, true))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let succ = self.succ; // FIXME(#6393)
|
let succ = self.succ; // FIXME(#6393)
|
||||||
let is_cleanup = self.is_cleanup;
|
|
||||||
let normal_ladder =
|
let normal_ladder =
|
||||||
self.drop_halfladder(unwind_ladder.as_ref().map(|x| &**x),
|
self.drop_halfladder(&unwind_ladder, succ, &fields);
|
||||||
succ, &fields, is_cleanup);
|
|
||||||
|
|
||||||
(normal_ladder.last().cloned().unwrap_or(succ),
|
(normal_ladder.last().cloned().unwrap_or(succ),
|
||||||
unwind_ladder.and_then(|l| l.last().cloned()).or(self.unwind))
|
unwind_ladder.last().cloned().unwrap_or(self.unwind))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_drop_for_tuple<'a>(&mut self, tys: &[Ty<'tcx>])
|
fn open_drop_for_tuple<'a>(&mut self, tys: &[Ty<'tcx>])
|
||||||
|
@ -320,13 +334,13 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
let interior_path = self.elaborator.deref_subpath(self.path);
|
let interior_path = self.elaborator.deref_subpath(self.path);
|
||||||
|
|
||||||
let succ = self.succ; // FIXME(#6393)
|
let succ = self.succ; // FIXME(#6393)
|
||||||
let is_cleanup = self.is_cleanup;
|
let unwind = self.unwind;
|
||||||
let succ = self.box_free_block(ty, succ, is_cleanup);
|
let succ = self.box_free_block(ty, succ, unwind);
|
||||||
let unwind_succ = self.unwind.map(|u| {
|
let unwind_succ = self.unwind.map(|unwind| {
|
||||||
self.box_free_block(ty, u, true)
|
self.box_free_block(ty, unwind, Unwind::InCleanup)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.drop_subpath(is_cleanup, &interior, interior_path, succ, unwind_succ)
|
self.drop_subpath(&interior, interior_path, succ, unwind_succ)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_drop_for_adt<'a>(&mut self, adt: &'tcx ty::AdtDef, substs: &'tcx Substs<'tcx>)
|
fn open_drop_for_adt<'a>(&mut self, adt: &'tcx ty::AdtDef, substs: &'tcx Substs<'tcx>)
|
||||||
|
@ -339,7 +353,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
kind: TerminatorKind::Unreachable
|
kind: TerminatorKind::Unreachable
|
||||||
}),
|
}),
|
||||||
is_cleanup: self.is_cleanup
|
is_cleanup: self.unwind.is_cleanup()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,7 +372,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
|
|
||||||
fn open_drop_for_adt_contents<'a>(&mut self, adt: &'tcx ty::AdtDef,
|
fn open_drop_for_adt_contents<'a>(&mut self, adt: &'tcx ty::AdtDef,
|
||||||
substs: &'tcx Substs<'tcx>)
|
substs: &'tcx Substs<'tcx>)
|
||||||
-> (BasicBlock, Option<BasicBlock>) {
|
-> (BasicBlock, Unwind) {
|
||||||
match adt.variants.len() {
|
match adt.variants.len() {
|
||||||
1 => {
|
1 => {
|
||||||
let fields = self.move_paths_for_fields(
|
let fields = self.move_paths_for_fields(
|
||||||
|
@ -370,13 +384,12 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.drop_ladder(fields)
|
self.drop_ladder(fields)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let is_cleanup = self.is_cleanup;
|
|
||||||
let succ = self.succ;
|
let succ = self.succ;
|
||||||
let unwind = self.unwind; // FIXME(#6393)
|
let unwind = self.unwind; // FIXME(#6393)
|
||||||
|
|
||||||
let mut values = Vec::with_capacity(adt.variants.len());
|
let mut values = Vec::with_capacity(adt.variants.len());
|
||||||
let mut normal_blocks = Vec::with_capacity(adt.variants.len());
|
let mut normal_blocks = Vec::with_capacity(adt.variants.len());
|
||||||
let mut unwind_blocks = if is_cleanup {
|
let mut unwind_blocks = if unwind.is_cleanup() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Vec::with_capacity(adt.variants.len()))
|
Some(Vec::with_capacity(adt.variants.len()))
|
||||||
|
@ -396,7 +409,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
&adt.variants[variant_index],
|
&adt.variants[variant_index],
|
||||||
substs);
|
substs);
|
||||||
values.push(discr);
|
values.push(discr);
|
||||||
if let Some(ref mut unwind_blocks) = unwind_blocks {
|
if let Unwind::To(unwind) = unwind {
|
||||||
// We can't use the half-ladder from the original
|
// We can't use the half-ladder from the original
|
||||||
// drop ladder, because this breaks the
|
// drop ladder, because this breaks the
|
||||||
// "funclet can't have 2 successor funclets"
|
// "funclet can't have 2 successor funclets"
|
||||||
|
@ -415,12 +428,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
// I want to minimize the divergence between MSVC
|
// I want to minimize the divergence between MSVC
|
||||||
// and non-MSVC.
|
// and non-MSVC.
|
||||||
|
|
||||||
let unwind = unwind.unwrap();
|
let unwind_blocks = unwind_blocks.as_mut().unwrap();
|
||||||
let halfladder = self.drop_halfladder(
|
let unwind_ladder = vec![Unwind::InCleanup; fields.len() + 1];
|
||||||
None, unwind, &fields, true);
|
let halfladder =
|
||||||
unwind_blocks.push(
|
self.drop_halfladder(&unwind_ladder, unwind, &fields);
|
||||||
halfladder.last().cloned().unwrap_or(unwind)
|
unwind_blocks.push(halfladder.last().cloned().unwrap_or(unwind));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let (normal, _) = self.drop_ladder(fields);
|
let (normal, _) = self.drop_ladder(fields);
|
||||||
normal_blocks.push(normal);
|
normal_blocks.push(normal);
|
||||||
|
@ -428,14 +440,16 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
// variant not found - drop the entire enum
|
// variant not found - drop the entire enum
|
||||||
if let None = otherwise {
|
if let None = otherwise {
|
||||||
otherwise = Some(self.complete_drop(
|
otherwise = Some(self.complete_drop(
|
||||||
is_cleanup,
|
|
||||||
Some(DropFlagMode::Shallow),
|
Some(DropFlagMode::Shallow),
|
||||||
succ));
|
succ,
|
||||||
unwind_otherwise = unwind.map(|unwind| self.complete_drop(
|
unwind));
|
||||||
true,
|
if let Unwind::To(unwind) = unwind {
|
||||||
Some(DropFlagMode::Shallow),
|
unwind_otherwise = Some(self.complete_drop(
|
||||||
unwind
|
Some(DropFlagMode::Shallow),
|
||||||
));
|
unwind,
|
||||||
|
Unwind::InCleanup
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -448,10 +462,10 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
values.pop();
|
values.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
(self.adt_switch_block(is_cleanup, adt, normal_blocks, &values, succ),
|
(self.adt_switch_block(adt, normal_blocks, &values, succ, unwind),
|
||||||
unwind_blocks.map(|unwind_blocks| {
|
unwind.map(|unwind| {
|
||||||
self.adt_switch_block(
|
self.adt_switch_block(
|
||||||
is_cleanup, adt, unwind_blocks, &values, unwind.unwrap()
|
adt, unwind_blocks.unwrap(), &values, unwind, Unwind::InCleanup
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -459,11 +473,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adt_switch_block(&mut self,
|
fn adt_switch_block(&mut self,
|
||||||
is_cleanup: bool,
|
|
||||||
adt: &'tcx ty::AdtDef,
|
adt: &'tcx ty::AdtDef,
|
||||||
blocks: Vec<BasicBlock>,
|
blocks: Vec<BasicBlock>,
|
||||||
values: &[ConstInt],
|
values: &[ConstInt],
|
||||||
succ: BasicBlock)
|
succ: BasicBlock,
|
||||||
|
unwind: Unwind)
|
||||||
-> BasicBlock {
|
-> BasicBlock {
|
||||||
// If there are multiple variants, then if something
|
// If there are multiple variants, then if something
|
||||||
// is present within the enum the discriminant, tracked
|
// is present within the enum the discriminant, tracked
|
||||||
|
@ -491,12 +505,12 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
targets: blocks,
|
targets: blocks,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
is_cleanup: is_cleanup,
|
is_cleanup: unwind.is_cleanup(),
|
||||||
});
|
});
|
||||||
self.drop_flag_test_block(is_cleanup, switch_block, succ)
|
self.drop_flag_test_block(switch_block, succ, unwind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn destructor_call_block<'a>(&mut self, (succ, unwind): (BasicBlock, Option<BasicBlock>))
|
fn destructor_call_block<'a>(&mut self, (succ, unwind): (BasicBlock, Unwind))
|
||||||
-> BasicBlock
|
-> BasicBlock
|
||||||
{
|
{
|
||||||
debug!("destructor_call_block({:?}, {:?})", self, succ);
|
debug!("destructor_call_block({:?}, {:?})", self, succ);
|
||||||
|
@ -527,11 +541,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.source_info.span),
|
self.source_info.span),
|
||||||
args: vec![Operand::Consume(Lvalue::Local(ref_lvalue))],
|
args: vec![Operand::Consume(Lvalue::Local(ref_lvalue))],
|
||||||
destination: Some((unit_temp, succ)),
|
destination: Some((unit_temp, succ)),
|
||||||
cleanup: unwind,
|
cleanup: unwind.into_option(),
|
||||||
},
|
},
|
||||||
source_info: self.source_info
|
source_info: self.source_info
|
||||||
}),
|
}),
|
||||||
is_cleanup: self.is_cleanup,
|
is_cleanup: unwind.is_cleanup(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,16 +555,15 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
/// can_go = index < len
|
/// can_go = index < len
|
||||||
/// if can_go then drop-block else succ
|
/// if can_go then drop-block else succ
|
||||||
/// drop-block:
|
/// drop-block:
|
||||||
/// ptr = &mut LV[len]
|
/// ptr = &mut LV[index]
|
||||||
/// index = index + 1
|
/// index = index + 1
|
||||||
/// drop(ptr)
|
/// drop(ptr)
|
||||||
fn drop_loop(&mut self,
|
fn drop_loop(&mut self,
|
||||||
unwind: Option<BasicBlock>,
|
|
||||||
succ: BasicBlock,
|
succ: BasicBlock,
|
||||||
index: &Lvalue<'tcx>,
|
index: &Lvalue<'tcx>,
|
||||||
length: &Lvalue<'tcx>,
|
length: &Lvalue<'tcx>,
|
||||||
ety: Ty<'tcx>,
|
ety: Ty<'tcx>,
|
||||||
is_cleanup: bool)
|
unwind: Unwind)
|
||||||
-> BasicBlock
|
-> BasicBlock
|
||||||
{
|
{
|
||||||
let use_ = |lv: &Lvalue<'tcx>| Operand::Consume(lv.clone());
|
let use_ = |lv: &Lvalue<'tcx>| Operand::Consume(lv.clone());
|
||||||
|
@ -576,10 +589,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
index.clone(), Rvalue::BinaryOp(BinOp::Add, use_(index), one)
|
index.clone(), Rvalue::BinaryOp(BinOp::Add, use_(index), one)
|
||||||
)},
|
)},
|
||||||
],
|
],
|
||||||
is_cleanup,
|
is_cleanup: unwind.is_cleanup(),
|
||||||
terminator: Some(Terminator {
|
terminator: Some(Terminator {
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
kind: TerminatorKind::Resume,
|
// this gets overwritten by drop elaboration.
|
||||||
|
kind: TerminatorKind::Unreachable,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -589,7 +603,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
can_go.clone(), Rvalue::BinaryOp(BinOp::Lt, use_(index), use_(length))
|
can_go.clone(), Rvalue::BinaryOp(BinOp::Lt, use_(index), use_(length))
|
||||||
)},
|
)},
|
||||||
],
|
],
|
||||||
is_cleanup,
|
is_cleanup: unwind.is_cleanup(),
|
||||||
terminator: Some(Terminator {
|
terminator: Some(Terminator {
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
kind: TerminatorKind::if_(tcx, use_(can_go), drop_block, succ)
|
kind: TerminatorKind::if_(tcx, use_(can_go), drop_block, succ)
|
||||||
|
@ -599,7 +613,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.elaborator.patch().patch_terminator(drop_block, TerminatorKind::Drop {
|
self.elaborator.patch().patch_terminator(drop_block, TerminatorKind::Drop {
|
||||||
location: ptr.clone().deref(),
|
location: ptr.clone().deref(),
|
||||||
target: loop_block,
|
target: loop_block,
|
||||||
unwind: unwind
|
unwind: unwind.into_option()
|
||||||
});
|
});
|
||||||
|
|
||||||
loop_block
|
loop_block
|
||||||
|
@ -614,12 +628,11 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
let length = &Lvalue::Local(self.new_temp(tcx.types.usize));
|
let length = &Lvalue::Local(self.new_temp(tcx.types.usize));
|
||||||
|
|
||||||
let unwind = self.unwind.map(|unwind| {
|
let unwind = self.unwind.map(|unwind| {
|
||||||
self.drop_loop(None, unwind, index, length, ety, true)
|
self.drop_loop(unwind, index, length, ety, Unwind::InCleanup)
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_cleanup = self.is_cleanup;
|
|
||||||
let succ = self.succ; // FIXME(#6393)
|
let succ = self.succ; // FIXME(#6393)
|
||||||
let loop_block = self.drop_loop(unwind, succ, index, length, ety, is_cleanup);
|
let loop_block = self.drop_loop(succ, index, length, ety, unwind);
|
||||||
|
|
||||||
let zero = self.constant_usize(0);
|
let zero = self.constant_usize(0);
|
||||||
let drop_block = self.elaborator.patch().new_block(BasicBlockData {
|
let drop_block = self.elaborator.patch().new_block(BasicBlockData {
|
||||||
|
@ -631,7 +644,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
index.clone(), Rvalue::Use(zero),
|
index.clone(), Rvalue::Use(zero),
|
||||||
)},
|
)},
|
||||||
],
|
],
|
||||||
is_cleanup,
|
is_cleanup: unwind.is_cleanup(),
|
||||||
terminator: Some(Terminator {
|
terminator: Some(Terminator {
|
||||||
source_info: self.source_info,
|
source_info: self.source_info,
|
||||||
kind: TerminatorKind::Goto { target: loop_block }
|
kind: TerminatorKind::Goto { target: loop_block }
|
||||||
|
@ -640,7 +653,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
|
|
||||||
// FIXME(#34708): handle partially-dropped array/slice elements.
|
// FIXME(#34708): handle partially-dropped array/slice elements.
|
||||||
self.drop_flag_test_and_reset_block(
|
self.drop_flag_test_and_reset_block(
|
||||||
is_cleanup, Some(DropFlagMode::Deep), drop_block, succ)
|
Some(DropFlagMode::Deep), drop_block, succ, unwind)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The slow-path - create an "open", elaborated drop for a type
|
/// The slow-path - create an "open", elaborated drop for a type
|
||||||
|
@ -653,8 +666,6 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
/// ADT, both in the success case or if one of the destructors fail.
|
/// ADT, both in the success case or if one of the destructors fail.
|
||||||
fn open_drop<'a>(&mut self) -> BasicBlock {
|
fn open_drop<'a>(&mut self) -> BasicBlock {
|
||||||
let ty = self.lvalue_ty(self.lvalue);
|
let ty = self.lvalue_ty(self.lvalue);
|
||||||
let is_cleanup = self.is_cleanup; // FIXME(#6393)
|
|
||||||
let succ = self.succ;
|
|
||||||
match ty.sty {
|
match ty.sty {
|
||||||
ty::TyClosure(def_id, substs) => {
|
ty::TyClosure(def_id, substs) => {
|
||||||
let tys : Vec<_> = substs.upvar_tys(def_id, self.tcx()).collect();
|
let tys : Vec<_> = substs.upvar_tys(def_id, self.tcx()).collect();
|
||||||
|
@ -670,7 +681,9 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.open_drop_for_adt(def, substs)
|
self.open_drop_for_adt(def, substs)
|
||||||
}
|
}
|
||||||
ty::TyDynamic(..) => {
|
ty::TyDynamic(..) => {
|
||||||
self.complete_drop(is_cleanup, Some(DropFlagMode::Deep), succ)
|
let unwind = self.unwind; // FIXME(#6393)
|
||||||
|
let succ = self.succ;
|
||||||
|
self.complete_drop(Some(DropFlagMode::Deep), succ, unwind)
|
||||||
}
|
}
|
||||||
ty::TyArray(ety, _) | ty::TySlice(ety) => {
|
ty::TyArray(ety, _) | ty::TySlice(ety) => {
|
||||||
self.open_drop_for_array(ety)
|
self.open_drop_for_array(ety)
|
||||||
|
@ -687,21 +700,21 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
/// if let Some(mode) = mode: FLAG(self.path)[mode] = false
|
/// if let Some(mode) = mode: FLAG(self.path)[mode] = false
|
||||||
/// drop(self.lv)
|
/// drop(self.lv)
|
||||||
fn complete_drop<'a>(&mut self,
|
fn complete_drop<'a>(&mut self,
|
||||||
is_cleanup: bool,
|
|
||||||
drop_mode: Option<DropFlagMode>,
|
drop_mode: Option<DropFlagMode>,
|
||||||
succ: BasicBlock) -> BasicBlock
|
succ: BasicBlock,
|
||||||
|
unwind: Unwind) -> BasicBlock
|
||||||
{
|
{
|
||||||
debug!("complete_drop({:?},{:?})", self, drop_mode);
|
debug!("complete_drop({:?},{:?})", self, drop_mode);
|
||||||
|
|
||||||
let drop_block = self.drop_block(is_cleanup, succ);
|
let drop_block = self.drop_block(succ, unwind);
|
||||||
self.drop_flag_test_and_reset_block(is_cleanup, drop_mode, drop_block, succ)
|
self.drop_flag_test_and_reset_block(drop_mode, drop_block, succ, unwind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_flag_test_and_reset_block(&mut self,
|
fn drop_flag_test_and_reset_block(&mut self,
|
||||||
is_cleanup: bool,
|
|
||||||
drop_mode: Option<DropFlagMode>,
|
drop_mode: Option<DropFlagMode>,
|
||||||
drop_block: BasicBlock,
|
drop_block: BasicBlock,
|
||||||
succ: BasicBlock) -> BasicBlock
|
succ: BasicBlock,
|
||||||
|
unwind: Unwind) -> BasicBlock
|
||||||
{
|
{
|
||||||
debug!("drop_flag_test_and_reset_block({:?},{:?})", self, drop_mode);
|
debug!("drop_flag_test_and_reset_block({:?},{:?})", self, drop_mode);
|
||||||
|
|
||||||
|
@ -710,14 +723,14 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.elaborator.clear_drop_flag(block_start, self.path, mode);
|
self.elaborator.clear_drop_flag(block_start, self.path, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.drop_flag_test_block(is_cleanup, drop_block, succ)
|
self.drop_flag_test_block(drop_block, succ, unwind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn elaborated_drop_block<'a>(&mut self) -> BasicBlock {
|
fn elaborated_drop_block<'a>(&mut self) -> BasicBlock {
|
||||||
debug!("elaborated_drop_block({:?})", self);
|
debug!("elaborated_drop_block({:?})", self);
|
||||||
let is_cleanup = self.is_cleanup; // FIXME(#6393)
|
let unwind = self.unwind; // FIXME(#6393)
|
||||||
let succ = self.succ;
|
let succ = self.succ;
|
||||||
let blk = self.drop_block(is_cleanup, succ);
|
let blk = self.drop_block(succ, unwind);
|
||||||
self.elaborate_drop(blk);
|
self.elaborate_drop(blk);
|
||||||
blk
|
blk
|
||||||
}
|
}
|
||||||
|
@ -726,17 +739,17 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
&mut self,
|
&mut self,
|
||||||
ty: Ty<'tcx>,
|
ty: Ty<'tcx>,
|
||||||
target: BasicBlock,
|
target: BasicBlock,
|
||||||
is_cleanup: bool
|
unwind: Unwind,
|
||||||
) -> BasicBlock {
|
) -> BasicBlock {
|
||||||
let block = self.unelaborated_free_block(ty, target, is_cleanup);
|
let block = self.unelaborated_free_block(ty, target, unwind);
|
||||||
self.drop_flag_test_block(is_cleanup, block, target)
|
self.drop_flag_test_block(block, target, unwind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unelaborated_free_block<'a>(
|
fn unelaborated_free_block<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ty: Ty<'tcx>,
|
ty: Ty<'tcx>,
|
||||||
target: BasicBlock,
|
target: BasicBlock,
|
||||||
is_cleanup: bool
|
unwind: Unwind
|
||||||
) -> BasicBlock {
|
) -> BasicBlock {
|
||||||
let tcx = self.tcx();
|
let tcx = self.tcx();
|
||||||
let unit_temp = Lvalue::Local(self.new_temp(tcx.mk_nil()));
|
let unit_temp = Lvalue::Local(self.new_temp(tcx.mk_nil()));
|
||||||
|
@ -749,31 +762,31 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
destination: Some((unit_temp, target)),
|
destination: Some((unit_temp, target)),
|
||||||
cleanup: None
|
cleanup: None
|
||||||
}; // FIXME(#6393)
|
}; // FIXME(#6393)
|
||||||
let free_block = self.new_block(is_cleanup, call);
|
let free_block = self.new_block(unwind, call);
|
||||||
|
|
||||||
let block_start = Location { block: free_block, statement_index: 0 };
|
let block_start = Location { block: free_block, statement_index: 0 };
|
||||||
self.elaborator.clear_drop_flag(block_start, self.path, DropFlagMode::Shallow);
|
self.elaborator.clear_drop_flag(block_start, self.path, DropFlagMode::Shallow);
|
||||||
free_block
|
free_block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_block<'a>(&mut self, is_cleanup: bool, succ: BasicBlock) -> BasicBlock {
|
fn drop_block<'a>(&mut self, target: BasicBlock, unwind: Unwind) -> BasicBlock {
|
||||||
let block = TerminatorKind::Drop {
|
let block = TerminatorKind::Drop {
|
||||||
location: self.lvalue.clone(),
|
location: self.lvalue.clone(),
|
||||||
target: succ,
|
target: target,
|
||||||
unwind: if is_cleanup { None } else { self.unwind }
|
unwind: unwind.into_option()
|
||||||
};
|
};
|
||||||
self.new_block(is_cleanup, block)
|
self.new_block(unwind, block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_flag_test_block(&mut self,
|
fn drop_flag_test_block(&mut self,
|
||||||
is_cleanup: bool,
|
|
||||||
on_set: BasicBlock,
|
on_set: BasicBlock,
|
||||||
on_unset: BasicBlock)
|
on_unset: BasicBlock,
|
||||||
|
unwind: Unwind)
|
||||||
-> BasicBlock
|
-> BasicBlock
|
||||||
{
|
{
|
||||||
let style = self.elaborator.drop_style(self.path, DropFlagMode::Shallow);
|
let style = self.elaborator.drop_style(self.path, DropFlagMode::Shallow);
|
||||||
debug!("drop_flag_test_block({:?},{:?},{:?}) - {:?}",
|
debug!("drop_flag_test_block({:?},{:?},{:?},{:?}) - {:?}",
|
||||||
self, is_cleanup, on_set, style);
|
self, on_set, on_unset, unwind, style);
|
||||||
|
|
||||||
match style {
|
match style {
|
||||||
DropStyle::Dead => on_unset,
|
DropStyle::Dead => on_unset,
|
||||||
|
@ -781,13 +794,13 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
DropStyle::Conditional | DropStyle::Open => {
|
DropStyle::Conditional | DropStyle::Open => {
|
||||||
let flag = self.elaborator.get_drop_flag(self.path).unwrap();
|
let flag = self.elaborator.get_drop_flag(self.path).unwrap();
|
||||||
let term = TerminatorKind::if_(self.tcx(), flag, on_set, on_unset);
|
let term = TerminatorKind::if_(self.tcx(), flag, on_set, on_unset);
|
||||||
self.new_block(is_cleanup, term)
|
self.new_block(unwind, term)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_block<'a>(&mut self,
|
fn new_block<'a>(&mut self,
|
||||||
is_cleanup: bool,
|
unwind: Unwind,
|
||||||
k: TerminatorKind<'tcx>)
|
k: TerminatorKind<'tcx>)
|
||||||
-> BasicBlock
|
-> BasicBlock
|
||||||
{
|
{
|
||||||
|
@ -796,7 +809,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
terminator: Some(Terminator {
|
terminator: Some(Terminator {
|
||||||
source_info: self.source_info, kind: k
|
source_info: self.source_info, kind: k
|
||||||
}),
|
}),
|
||||||
is_cleanup: is_cleanup
|
is_cleanup: unwind.is_cleanup()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -809,7 +822,7 @@ impl<'l, 'b, 'tcx, D> DropCtxt<'l, 'b, 'tcx, D>
|
||||||
self.elaborator.patch().terminator_loc(mir, bb)
|
self.elaborator.patch().terminator_loc(mir, bb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn constant_usize(&self, val: usize) -> Operand<'tcx> {
|
fn constant_usize(&self, val: u16) -> Operand<'tcx> {
|
||||||
Operand::Constant(box Constant {
|
Operand::Constant(box Constant {
|
||||||
span: self.source_info.span,
|
span: self.source_info.span,
|
||||||
ty: self.tcx().types.usize,
|
ty: self.tcx().types.usize,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue