Remove pinned VRegs. (#108)
This commit is contained in:
@@ -366,25 +366,8 @@ impl CheckerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initial_with_pinned_vregs<F: Function>(f: &F) -> CheckerState {
|
fn initial() -> Self {
|
||||||
// Scan the function, looking for all vregs that are pinned
|
CheckerState::Allocations(FxHashMap::default())
|
||||||
// vregs, gathering them with their PRegs.
|
|
||||||
let mut pinned_vregs: FxHashMap<VReg, PReg> = FxHashMap::default();
|
|
||||||
visit_all_vregs(f, |vreg: VReg| {
|
|
||||||
if let Some(preg) = f.is_pinned_vreg(vreg) {
|
|
||||||
pinned_vregs.insert(vreg, preg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut allocs = FxHashMap::default();
|
|
||||||
for (vreg, preg) in pinned_vregs {
|
|
||||||
allocs.insert(
|
|
||||||
Allocation::reg(preg),
|
|
||||||
CheckerValue::VRegs(std::iter::once(vreg).collect()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckerState::Allocations(allocs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,7 +840,7 @@ impl<'a, F: Function> Checker<'a, F> {
|
|||||||
reftyped_vregs.insert(vreg);
|
reftyped_vregs.insert(vreg);
|
||||||
}
|
}
|
||||||
|
|
||||||
bb_in.insert(f.entry_block(), CheckerState::initial_with_pinned_vregs(f));
|
bb_in.insert(f.entry_block(), CheckerState::default());
|
||||||
|
|
||||||
let mut stack_pregs = PRegSet::empty();
|
let mut stack_pregs = PRegSet::empty();
|
||||||
for &preg in &machine_env.fixed_stack_slots {
|
for &preg in &machine_env.fixed_stack_slots {
|
||||||
@@ -932,21 +915,11 @@ impl<'a, F: Function> Checker<'a, F> {
|
|||||||
// move/edit framework, so we don't get allocs for these moves
|
// move/edit framework, so we don't get allocs for these moves
|
||||||
// in the post-regalloc output, and the embedder is not
|
// in the post-regalloc output, and the embedder is not
|
||||||
// supposed to emit the moves. But we *do* want to check the
|
// supposed to emit the moves. But we *do* want to check the
|
||||||
// semantic implications, namely definition of new vregs and,
|
// semantic implications, namely definition of new vregs. So
|
||||||
// for moves to/from pinned vregs, the implied register
|
// we emit `ProgramMove` ops that do just this.
|
||||||
// constraints. So we emit `ProgramMove` ops that do just
|
|
||||||
// this.
|
|
||||||
if let Some((src, dst)) = self.f.is_move(inst) {
|
if let Some((src, dst)) = self.f.is_move(inst) {
|
||||||
let src_preg = self.f.is_pinned_vreg(src.vreg());
|
let src_op = Operand::any_use(src.vreg());
|
||||||
let src_op = match src_preg {
|
let dst_op = Operand::any_def(dst.vreg());
|
||||||
Some(preg) => Operand::reg_fixed_use(src.vreg(), preg),
|
|
||||||
None => Operand::any_use(src.vreg()),
|
|
||||||
};
|
|
||||||
let dst_preg = self.f.is_pinned_vreg(dst.vreg());
|
|
||||||
let dst_op = match dst_preg {
|
|
||||||
Some(preg) => Operand::reg_fixed_def(dst.vreg(), preg),
|
|
||||||
None => Operand::any_def(dst.vreg()),
|
|
||||||
};
|
|
||||||
let checkinst = CheckerInst::ProgramMove {
|
let checkinst = CheckerInst::ProgramMove {
|
||||||
inst,
|
inst,
|
||||||
src: src_op,
|
src: src_op,
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ impl AdaptiveMap {
|
|||||||
&Self::Large(ref map) => AdaptiveMapIter::Large(map.iter()),
|
&Self::Large(ref map) => AdaptiveMapIter::Large(map.iter()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
AdaptiveMap::Small { values, .. } => values.iter().all(|&value| value == 0),
|
||||||
|
AdaptiveMap::Large(m) => m.values().all(|&value| value == 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AdaptiveMapIter<'a> {
|
enum AdaptiveMapIter<'a> {
|
||||||
@@ -268,6 +275,11 @@ impl IndexSet {
|
|||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is the set empty?
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
self.elems.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SetBitsIter(u64);
|
pub struct SetBitsIter(u64);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
//! Live-range computation.
|
//! Live-range computation.
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
CodeRange, Env, InsertMovePrio, LiveBundle, LiveBundleIndex, LiveRange, LiveRangeFlag,
|
CodeRange, Env, LiveBundle, LiveBundleIndex, LiveRange, LiveRangeFlag, LiveRangeIndex,
|
||||||
LiveRangeIndex, LiveRangeKey, LiveRangeListEntry, LiveRangeSet, PRegData, PRegIndex, RegClass,
|
LiveRangeKey, LiveRangeListEntry, LiveRangeSet, PRegData, PRegIndex, RegClass, SpillSetIndex,
|
||||||
SpillSetIndex, Use, VRegData, VRegIndex, SLOT_NONE,
|
Use, VRegData, VRegIndex, SLOT_NONE,
|
||||||
};
|
};
|
||||||
use crate::indexset::IndexSet;
|
use crate::indexset::IndexSet;
|
||||||
use crate::ion::data_structures::{
|
use crate::ion::data_structures::{
|
||||||
@@ -403,15 +403,13 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
self.liveins[block.index()] = live;
|
self.liveins[block.index()] = live;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that there are no liveins to the entry block, except
|
// Check that there are no liveins to the entry block.
|
||||||
// for pinned vregs. (The client should create a virtual
|
if !self.liveins[self.func.entry_block().index()].is_empty() {
|
||||||
// instruction that defines any other liveins if necessary.)
|
trace!(
|
||||||
for livein in self.liveins[self.func.entry_block().index()].iter() {
|
"non-empty liveins to entry block: {:?}",
|
||||||
let livein = self.vreg(VRegIndex::new(livein));
|
self.liveins[self.func.entry_block().index()]
|
||||||
if self.func.is_pinned_vreg(livein).is_none() {
|
);
|
||||||
trace!("non-pinned-vreg livein to entry block: {}", livein);
|
return Err(RegAllocError::EntryLivein);
|
||||||
return Err(RegAllocError::EntryLivein);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -535,392 +533,128 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
debug_assert_eq!(dst.kind(), OperandKind::Def);
|
debug_assert_eq!(dst.kind(), OperandKind::Def);
|
||||||
debug_assert_eq!(dst.pos(), OperandPos::Late);
|
debug_assert_eq!(dst.pos(), OperandPos::Late);
|
||||||
|
|
||||||
let src_pinned = self.func.is_pinned_vreg(src.vreg());
|
// Redefine src and dst operands to have
|
||||||
let dst_pinned = self.func.is_pinned_vreg(dst.vreg());
|
// positions of After and Before respectively
|
||||||
|
// (see note below), and to have Any
|
||||||
|
// constraints if they were originally Reg.
|
||||||
|
let src_constraint = match src.constraint() {
|
||||||
|
OperandConstraint::Reg => OperandConstraint::Any,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let dst_constraint = match dst.constraint() {
|
||||||
|
OperandConstraint::Reg => OperandConstraint::Any,
|
||||||
|
x => x,
|
||||||
|
};
|
||||||
|
let src = Operand::new(
|
||||||
|
src.vreg(),
|
||||||
|
src_constraint,
|
||||||
|
OperandKind::Use,
|
||||||
|
OperandPos::Late,
|
||||||
|
);
|
||||||
|
let dst = Operand::new(
|
||||||
|
dst.vreg(),
|
||||||
|
dst_constraint,
|
||||||
|
OperandKind::Def,
|
||||||
|
OperandPos::Early,
|
||||||
|
);
|
||||||
|
|
||||||
match (src_pinned, dst_pinned) {
|
if self.annotations_enabled {
|
||||||
// If both src and dest are pinned, emit
|
self.annotate(
|
||||||
// the move right here, right now.
|
ProgPoint::after(inst),
|
||||||
(Some(src_preg), Some(dst_preg)) => {
|
format!(
|
||||||
// Update LRs.
|
" prog-move v{} ({:?}) -> v{} ({:?})",
|
||||||
if !live.get(src.vreg().vreg()) {
|
src.vreg().vreg(),
|
||||||
let lr = self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(src.vreg().vreg()),
|
|
||||||
CodeRange {
|
|
||||||
from: self.cfginfo.block_entry[block.index()],
|
|
||||||
to: ProgPoint::after(inst),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
live.set(src.vreg().vreg(), true);
|
|
||||||
vreg_ranges[src.vreg().vreg()] = lr;
|
|
||||||
}
|
|
||||||
if live.get(dst.vreg().vreg()) {
|
|
||||||
let lr = vreg_ranges[dst.vreg().vreg()];
|
|
||||||
self.ranges[lr.index()].range.from = ProgPoint::after(inst);
|
|
||||||
live.set(dst.vreg().vreg(), false);
|
|
||||||
} else {
|
|
||||||
self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(dst.vreg().vreg()),
|
|
||||||
CodeRange {
|
|
||||||
from: ProgPoint::after(inst),
|
|
||||||
to: ProgPoint::before(inst.next()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.insert_move(
|
|
||||||
ProgPoint::before(inst),
|
|
||||||
InsertMovePrio::MultiFixedRegInitial,
|
|
||||||
Allocation::reg(src_preg),
|
|
||||||
Allocation::reg(dst_preg),
|
|
||||||
dst.vreg(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If exactly one of source and dest (but
|
|
||||||
// not both) is a pinned-vreg, convert
|
|
||||||
// this into a ghost use on the other vreg
|
|
||||||
// with a FixedReg constraint.
|
|
||||||
(Some(preg), None) | (None, Some(preg)) => {
|
|
||||||
trace!(
|
|
||||||
" -> exactly one of src/dst is pinned; converting to ghost use"
|
|
||||||
);
|
|
||||||
let (vreg, pinned_vreg, kind, pos, progpoint) =
|
|
||||||
if src_pinned.is_some() {
|
|
||||||
// Source is pinned: this is a def on the dst with a pinned preg.
|
|
||||||
(
|
|
||||||
dst.vreg(),
|
|
||||||
src.vreg(),
|
|
||||||
OperandKind::Def,
|
|
||||||
OperandPos::Late,
|
|
||||||
ProgPoint::before(inst),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Dest is pinned: this is a use on the src with a pinned preg.
|
|
||||||
(
|
|
||||||
src.vreg(),
|
|
||||||
dst.vreg(),
|
|
||||||
OperandKind::Use,
|
|
||||||
OperandPos::Early,
|
|
||||||
ProgPoint::after(inst),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let constraint = OperandConstraint::FixedReg(preg);
|
|
||||||
let operand = Operand::new(vreg, constraint, kind, pos);
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
concat!(
|
|
||||||
" -> preg {:?} vreg {:?} kind {:?} ",
|
|
||||||
"pos {:?} progpoint {:?} constraint {:?} operand {:?}"
|
|
||||||
),
|
|
||||||
preg,
|
|
||||||
vreg,
|
|
||||||
kind,
|
|
||||||
pos,
|
|
||||||
progpoint,
|
|
||||||
constraint,
|
|
||||||
operand
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the LR for the vreg; if none, create one.
|
|
||||||
let mut lr = vreg_ranges[vreg.vreg()];
|
|
||||||
if !live.get(vreg.vreg()) {
|
|
||||||
let from = match kind {
|
|
||||||
OperandKind::Use => self.cfginfo.block_entry[block.index()],
|
|
||||||
OperandKind::Def => progpoint,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let to = progpoint.next();
|
|
||||||
lr = self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(vreg.vreg()),
|
|
||||||
CodeRange { from, to },
|
|
||||||
);
|
|
||||||
trace!(" -> dead; created LR");
|
|
||||||
}
|
|
||||||
trace!(" -> LR {:?}", lr);
|
|
||||||
|
|
||||||
self.insert_use_into_liverange(
|
|
||||||
lr,
|
|
||||||
Use::new(operand, progpoint, SLOT_NONE),
|
|
||||||
);
|
|
||||||
|
|
||||||
if kind == OperandKind::Def {
|
|
||||||
live.set(vreg.vreg(), false);
|
|
||||||
if self.ranges[lr.index()].range.from
|
|
||||||
== self.cfginfo.block_entry[block.index()]
|
|
||||||
{
|
|
||||||
self.ranges[lr.index()].range.from = progpoint;
|
|
||||||
}
|
|
||||||
self.ranges[lr.index()].set_flag(LiveRangeFlag::StartsAtDef);
|
|
||||||
} else {
|
|
||||||
live.set(vreg.vreg(), true);
|
|
||||||
vreg_ranges[vreg.vreg()] = lr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle liveness of the other vreg. Note
|
|
||||||
// that this is somewhat special. For the
|
|
||||||
// destination case, we want the pinned
|
|
||||||
// vreg's LR to start just *after* the
|
|
||||||
// operand we inserted above, because
|
|
||||||
// otherwise it would overlap, and
|
|
||||||
// interfere, and prevent allocation. For
|
|
||||||
// the source case, we want to "poke a
|
|
||||||
// hole" in the LR: if it's live going
|
|
||||||
// downward, end it just after the operand
|
|
||||||
// and restart it before; if it isn't
|
|
||||||
// (this is the last use), start it
|
|
||||||
// before.
|
|
||||||
if kind == OperandKind::Def {
|
|
||||||
trace!(" -> src on pinned vreg {:?}", pinned_vreg);
|
|
||||||
// The *other* vreg is a def, so the pinned-vreg
|
|
||||||
// mention is a use. If already live,
|
|
||||||
// end the existing LR just *after*
|
|
||||||
// the `progpoint` defined above and
|
|
||||||
// start a new one just *before* the
|
|
||||||
// `progpoint` defined above,
|
|
||||||
// preserving the start. If not, start
|
|
||||||
// a new one live back to the top of
|
|
||||||
// the block, starting just before
|
|
||||||
// `progpoint`.
|
|
||||||
if live.get(pinned_vreg.vreg()) {
|
|
||||||
let pinned_lr = vreg_ranges[pinned_vreg.vreg()];
|
|
||||||
let orig_start = self.ranges[pinned_lr.index()].range.from;
|
|
||||||
// Following instruction start
|
|
||||||
// (so we don't transition in
|
|
||||||
// middle of inst).
|
|
||||||
let new_start = ProgPoint::before(progpoint.inst().next());
|
|
||||||
trace!(
|
|
||||||
" -> live with LR {:?}; truncating to start at {:?}",
|
|
||||||
pinned_lr,
|
|
||||||
new_start,
|
|
||||||
);
|
|
||||||
self.ranges[pinned_lr.index()].range.from = new_start;
|
|
||||||
|
|
||||||
let new_lr = self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(pinned_vreg.vreg()),
|
|
||||||
CodeRange {
|
|
||||||
from: orig_start,
|
|
||||||
to: progpoint,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
vreg_ranges[pinned_vreg.vreg()] = new_lr;
|
|
||||||
trace!(" -> created LR {:?} with remaining range from {:?} to {:?}", new_lr, orig_start, progpoint);
|
|
||||||
|
|
||||||
// Add an edit right now to indicate that at
|
|
||||||
// this program point, the given
|
|
||||||
// preg is now known as that vreg,
|
|
||||||
// not the preg, but immediately
|
|
||||||
// after, it is known as the preg
|
|
||||||
// again. This is used by the
|
|
||||||
// checker.
|
|
||||||
self.insert_move(
|
|
||||||
ProgPoint::after(inst),
|
|
||||||
InsertMovePrio::Regular,
|
|
||||||
Allocation::reg(preg),
|
|
||||||
Allocation::reg(preg),
|
|
||||||
dst.vreg(),
|
|
||||||
);
|
|
||||||
self.insert_move(
|
|
||||||
ProgPoint::before(inst.next()),
|
|
||||||
InsertMovePrio::MultiFixedRegInitial,
|
|
||||||
Allocation::reg(preg),
|
|
||||||
Allocation::reg(preg),
|
|
||||||
src.vreg(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if inst > self.cfginfo.block_entry[block.index()].inst() {
|
|
||||||
let new_lr = self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(pinned_vreg.vreg()),
|
|
||||||
CodeRange {
|
|
||||||
from: self.cfginfo.block_entry[block.index()],
|
|
||||||
to: progpoint,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
vreg_ranges[pinned_vreg.vreg()] = new_lr;
|
|
||||||
live.set(pinned_vreg.vreg(), true);
|
|
||||||
trace!(" -> was not live; created new LR {:?}", new_lr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an edit right now to indicate that at
|
|
||||||
// this program point, the given
|
|
||||||
// preg is now known as that vreg,
|
|
||||||
// not the preg. This is used by
|
|
||||||
// the checker.
|
|
||||||
self.insert_move(
|
|
||||||
ProgPoint::after(inst),
|
|
||||||
InsertMovePrio::BlockParam,
|
|
||||||
Allocation::reg(preg),
|
|
||||||
Allocation::reg(preg),
|
|
||||||
dst.vreg(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trace!(" -> dst on pinned vreg {:?}", pinned_vreg);
|
|
||||||
// The *other* vreg is a use, so the pinned-vreg
|
|
||||||
// mention is a def. Truncate its LR
|
|
||||||
// just *after* the `progpoint`
|
|
||||||
// defined above.
|
|
||||||
if live.get(pinned_vreg.vreg()) {
|
|
||||||
let pinned_lr = vreg_ranges[pinned_vreg.vreg()];
|
|
||||||
self.ranges[pinned_lr.index()].range.from =
|
|
||||||
progpoint.next();
|
|
||||||
trace!(
|
|
||||||
" -> was live with LR {:?}; truncated start to {:?}",
|
|
||||||
pinned_lr,
|
|
||||||
progpoint.next()
|
|
||||||
);
|
|
||||||
live.set(pinned_vreg.vreg(), false);
|
|
||||||
|
|
||||||
// Add a no-op edit right now to indicate that
|
|
||||||
// at this program point, the
|
|
||||||
// given preg is now known as that
|
|
||||||
// preg, not the vreg. This is
|
|
||||||
// used by the checker.
|
|
||||||
self.insert_move(
|
|
||||||
ProgPoint::before(inst.next()),
|
|
||||||
InsertMovePrio::PostRegular,
|
|
||||||
Allocation::reg(preg),
|
|
||||||
Allocation::reg(preg),
|
|
||||||
dst.vreg(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Otherwise, if dead, no need to create
|
|
||||||
// a dummy LR -- there is no
|
|
||||||
// reservation to make (the other vreg
|
|
||||||
// will land in the reg with the
|
|
||||||
// fixed-reg operand constraint, but
|
|
||||||
// it's a dead move anyway).
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordinary move between two non-pinned vregs.
|
|
||||||
(None, None) => {
|
|
||||||
// Redefine src and dst operands to have
|
|
||||||
// positions of After and Before respectively
|
|
||||||
// (see note below), and to have Any
|
|
||||||
// constraints if they were originally Reg.
|
|
||||||
let src_constraint = match src.constraint() {
|
|
||||||
OperandConstraint::Reg => OperandConstraint::Any,
|
|
||||||
x => x,
|
|
||||||
};
|
|
||||||
let dst_constraint = match dst.constraint() {
|
|
||||||
OperandConstraint::Reg => OperandConstraint::Any,
|
|
||||||
x => x,
|
|
||||||
};
|
|
||||||
let src = Operand::new(
|
|
||||||
src.vreg(),
|
|
||||||
src_constraint,
|
src_constraint,
|
||||||
OperandKind::Use,
|
dst.vreg().vreg(),
|
||||||
OperandPos::Late,
|
|
||||||
);
|
|
||||||
let dst = Operand::new(
|
|
||||||
dst.vreg(),
|
|
||||||
dst_constraint,
|
dst_constraint,
|
||||||
OperandKind::Def,
|
),
|
||||||
OperandPos::Early,
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
if self.annotations_enabled {
|
// N.B.: in order to integrate with the move
|
||||||
self.annotate(
|
// resolution that joins LRs in general, we
|
||||||
ProgPoint::after(inst),
|
// conceptually treat the move as happening
|
||||||
format!(
|
// between the move inst's After and the next
|
||||||
" prog-move v{} ({:?}) -> v{} ({:?})",
|
// inst's Before. Thus the src LR goes up to
|
||||||
src.vreg().vreg(),
|
// (exclusive) next-inst-pre, and the dst LR
|
||||||
src_constraint,
|
// starts at next-inst-pre. We have to take
|
||||||
dst.vreg().vreg(),
|
// care in our move insertion to handle this
|
||||||
dst_constraint,
|
// like other inter-inst moves, i.e., at
|
||||||
),
|
// `Regular` priority, so it properly happens
|
||||||
);
|
// in parallel with other inter-LR moves.
|
||||||
}
|
//
|
||||||
|
// Why the progpoint between move and next
|
||||||
|
// inst, and not the progpoint between prev
|
||||||
|
// inst and move? Because a move can be the
|
||||||
|
// first inst in a block, but cannot be the
|
||||||
|
// last; so the following progpoint is always
|
||||||
|
// within the same block, while the previous
|
||||||
|
// one may be an inter-block point (and the
|
||||||
|
// After of the prev inst in a different
|
||||||
|
// block).
|
||||||
|
|
||||||
// N.B.: in order to integrate with the move
|
// Handle the def w.r.t. liveranges: trim the
|
||||||
// resolution that joins LRs in general, we
|
// start of the range and mark it dead at this
|
||||||
// conceptually treat the move as happening
|
// point in our backward scan.
|
||||||
// between the move inst's After and the next
|
let pos = ProgPoint::before(inst.next());
|
||||||
// inst's Before. Thus the src LR goes up to
|
let mut dst_lr = vreg_ranges[dst.vreg().vreg()];
|
||||||
// (exclusive) next-inst-pre, and the dst LR
|
if !live.get(dst.vreg().vreg()) {
|
||||||
// starts at next-inst-pre. We have to take
|
let from = pos;
|
||||||
// care in our move insertion to handle this
|
let to = pos.next();
|
||||||
// like other inter-inst moves, i.e., at
|
dst_lr = self.add_liverange_to_vreg(
|
||||||
// `Regular` priority, so it properly happens
|
VRegIndex::new(dst.vreg().vreg()),
|
||||||
// in parallel with other inter-LR moves.
|
CodeRange { from, to },
|
||||||
//
|
);
|
||||||
// Why the progpoint between move and next
|
trace!(" -> invalid LR for def; created {:?}", dst_lr);
|
||||||
// inst, and not the progpoint between prev
|
}
|
||||||
// inst and move? Because a move can be the
|
trace!(" -> has existing LR {:?}", dst_lr);
|
||||||
// first inst in a block, but cannot be the
|
// Trim the LR to start here.
|
||||||
// last; so the following progpoint is always
|
if self.ranges[dst_lr.index()].range.from
|
||||||
// within the same block, while the previous
|
== self.cfginfo.block_entry[block.index()]
|
||||||
// one may be an inter-block point (and the
|
{
|
||||||
// After of the prev inst in a different
|
trace!(" -> started at block start; trimming to {:?}", pos);
|
||||||
// block).
|
self.ranges[dst_lr.index()].range.from = pos;
|
||||||
|
}
|
||||||
|
self.ranges[dst_lr.index()].set_flag(LiveRangeFlag::StartsAtDef);
|
||||||
|
live.set(dst.vreg().vreg(), false);
|
||||||
|
vreg_ranges[dst.vreg().vreg()] = LiveRangeIndex::invalid();
|
||||||
|
|
||||||
// Handle the def w.r.t. liveranges: trim the
|
// Handle the use w.r.t. liveranges: make it live
|
||||||
// start of the range and mark it dead at this
|
// and create an initial LR back to the start of
|
||||||
// point in our backward scan.
|
// the block.
|
||||||
let pos = ProgPoint::before(inst.next());
|
let pos = ProgPoint::after(inst);
|
||||||
let mut dst_lr = vreg_ranges[dst.vreg().vreg()];
|
let src_lr = if !live.get(src.vreg().vreg()) {
|
||||||
if !live.get(dst.vreg().vreg()) {
|
let range = CodeRange {
|
||||||
let from = pos;
|
from: self.cfginfo.block_entry[block.index()],
|
||||||
let to = pos.next();
|
to: pos.next(),
|
||||||
dst_lr = self.add_liverange_to_vreg(
|
};
|
||||||
VRegIndex::new(dst.vreg().vreg()),
|
let src_lr = self
|
||||||
CodeRange { from, to },
|
.add_liverange_to_vreg(VRegIndex::new(src.vreg().vreg()), range);
|
||||||
);
|
vreg_ranges[src.vreg().vreg()] = src_lr;
|
||||||
trace!(" -> invalid LR for def; created {:?}", dst_lr);
|
src_lr
|
||||||
}
|
} else {
|
||||||
trace!(" -> has existing LR {:?}", dst_lr);
|
vreg_ranges[src.vreg().vreg()]
|
||||||
// Trim the LR to start here.
|
};
|
||||||
if self.ranges[dst_lr.index()].range.from
|
|
||||||
== self.cfginfo.block_entry[block.index()]
|
|
||||||
{
|
|
||||||
trace!(" -> started at block start; trimming to {:?}", pos);
|
|
||||||
self.ranges[dst_lr.index()].range.from = pos;
|
|
||||||
}
|
|
||||||
self.ranges[dst_lr.index()].set_flag(LiveRangeFlag::StartsAtDef);
|
|
||||||
live.set(dst.vreg().vreg(), false);
|
|
||||||
vreg_ranges[dst.vreg().vreg()] = LiveRangeIndex::invalid();
|
|
||||||
|
|
||||||
// Handle the use w.r.t. liveranges: make it live
|
trace!(" -> src LR {:?}", src_lr);
|
||||||
// and create an initial LR back to the start of
|
|
||||||
// the block.
|
|
||||||
let pos = ProgPoint::after(inst);
|
|
||||||
let src_lr = if !live.get(src.vreg().vreg()) {
|
|
||||||
let range = CodeRange {
|
|
||||||
from: self.cfginfo.block_entry[block.index()],
|
|
||||||
to: pos.next(),
|
|
||||||
};
|
|
||||||
let src_lr = self.add_liverange_to_vreg(
|
|
||||||
VRegIndex::new(src.vreg().vreg()),
|
|
||||||
range,
|
|
||||||
);
|
|
||||||
vreg_ranges[src.vreg().vreg()] = src_lr;
|
|
||||||
src_lr
|
|
||||||
} else {
|
|
||||||
vreg_ranges[src.vreg().vreg()]
|
|
||||||
};
|
|
||||||
|
|
||||||
trace!(" -> src LR {:?}", src_lr);
|
// Add to live-set.
|
||||||
|
let src_is_dead_after_move = !live.get(src.vreg().vreg());
|
||||||
|
live.set(src.vreg().vreg(), true);
|
||||||
|
|
||||||
// Add to live-set.
|
// Add to program-moves lists.
|
||||||
let src_is_dead_after_move = !live.get(src.vreg().vreg());
|
self.prog_move_srcs.push((
|
||||||
live.set(src.vreg().vreg(), true);
|
(VRegIndex::new(src.vreg().vreg()), inst),
|
||||||
|
Allocation::none(),
|
||||||
// Add to program-moves lists.
|
));
|
||||||
self.prog_move_srcs.push((
|
self.prog_move_dsts.push((
|
||||||
(VRegIndex::new(src.vreg().vreg()), inst),
|
(VRegIndex::new(dst.vreg().vreg()), inst.next()),
|
||||||
Allocation::none(),
|
Allocation::none(),
|
||||||
));
|
));
|
||||||
self.prog_move_dsts.push((
|
self.stats.prog_moves += 1;
|
||||||
(VRegIndex::new(dst.vreg().vreg()), inst.next()),
|
if src_is_dead_after_move {
|
||||||
Allocation::none(),
|
self.stats.prog_moves_dead_src += 1;
|
||||||
));
|
self.prog_move_merges.push((src_lr, dst_lr));
|
||||||
self.stats.prog_moves += 1;
|
|
||||||
if src_is_dead_after_move {
|
|
||||||
self.stats.prog_moves_dead_src += 1;
|
|
||||||
self.prog_move_merges.push((src_lr, dst_lr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1240,9 +974,6 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
|
|
||||||
// Insert safepoint virtual stack uses, if needed.
|
// Insert safepoint virtual stack uses, if needed.
|
||||||
for &vreg in self.func.reftype_vregs() {
|
for &vreg in self.func.reftype_vregs() {
|
||||||
if self.func.is_pinned_vreg(vreg).is_some() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let vreg = VRegIndex::new(vreg.vreg());
|
let vreg = VRegIndex::new(vreg.vreg());
|
||||||
let mut inserted = false;
|
let mut inserted = false;
|
||||||
let mut safepoint_idx = 0;
|
let mut safepoint_idx = 0;
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
//! Bundle merging.
|
//! Bundle merging.
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Env, LiveBundleIndex, LiveRangeIndex, LiveRangeKey, SpillSet, SpillSetIndex, SpillSlotIndex,
|
Env, LiveBundleIndex, LiveRangeIndex, SpillSet, SpillSetIndex, SpillSlotIndex, VRegIndex,
|
||||||
VRegIndex,
|
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
ion::data_structures::BlockparamOut, Function, Inst, OperandConstraint, OperandKind, PReg,
|
ion::data_structures::BlockparamOut, Function, Inst, OperandConstraint, OperandKind, PReg,
|
||||||
@@ -245,19 +244,6 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a pinned vreg, go ahead and add it to the
|
|
||||||
// commitment map, and avoid creating a bundle entirely.
|
|
||||||
if let Some(preg) = self.func.is_pinned_vreg(self.vreg(vreg)) {
|
|
||||||
for entry in &self.vregs[vreg.index()].ranges {
|
|
||||||
let key = LiveRangeKey::from_range(&entry.range);
|
|
||||||
self.pregs[preg.index()]
|
|
||||||
.allocations
|
|
||||||
.btree
|
|
||||||
.insert(key, LiveRangeIndex::invalid());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundle = self.create_bundle();
|
let bundle = self.create_bundle();
|
||||||
self.bundles[bundle.index()].ranges = self.vregs[vreg.index()].ranges.clone();
|
self.bundles[bundle.index()].ranges = self.vregs[vreg.index()].ranges.clone();
|
||||||
trace!("vreg v{} gets bundle{}", vreg.index(), bundle.index());
|
trace!("vreg v{} gets bundle{}", vreg.index(), bundle.index());
|
||||||
@@ -325,11 +311,6 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
if let OperandConstraint::Reuse(reuse_idx) = op.constraint() {
|
if let OperandConstraint::Reuse(reuse_idx) = op.constraint() {
|
||||||
let src_vreg = op.vreg();
|
let src_vreg = op.vreg();
|
||||||
let dst_vreg = self.func.inst_operands(inst)[reuse_idx].vreg();
|
let dst_vreg = self.func.inst_operands(inst)[reuse_idx].vreg();
|
||||||
if self.func.is_pinned_vreg(src_vreg).is_some()
|
|
||||||
|| self.func.is_pinned_vreg(dst_vreg).is_some()
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"trying to merge reused-input def: src {} to dst {}",
|
"trying to merge reused-input def: src {} to dst {}",
|
||||||
@@ -382,26 +363,6 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
dst
|
dst
|
||||||
);
|
);
|
||||||
|
|
||||||
let dst_vreg = self.vreg(self.ranges[dst.index()].vreg);
|
|
||||||
let src_vreg = self.vreg(self.ranges[src.index()].vreg);
|
|
||||||
if self.func.is_pinned_vreg(src_vreg).is_some()
|
|
||||||
&& self.func.is_pinned_vreg(dst_vreg).is_some()
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(preg) = self.func.is_pinned_vreg(src_vreg) {
|
|
||||||
let dest_bundle = self.ranges[dst.index()].bundle;
|
|
||||||
let spillset = self.bundles[dest_bundle.index()].spillset;
|
|
||||||
self.spillsets[spillset.index()].reg_hint = preg;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(preg) = self.func.is_pinned_vreg(dst_vreg) {
|
|
||||||
let src_bundle = self.ranges[src.index()].bundle;
|
|
||||||
let spillset = self.bundles[src_bundle.index()].spillset;
|
|
||||||
self.spillsets[spillset.index()].reg_hint = preg;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src_bundle = self.ranges[src.index()].bundle;
|
let src_bundle = self.ranges[src.index()].bundle;
|
||||||
debug_assert!(src_bundle.is_valid());
|
debug_assert!(src_bundle.is_valid());
|
||||||
let dest_bundle = self.ranges[dst.index()].bundle;
|
let dest_bundle = self.ranges[dst.index()].bundle;
|
||||||
|
|||||||
172
src/ion/moves.rs
172
src/ion/moves.rs
@@ -187,8 +187,6 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pinned_alloc = self.func.is_pinned_vreg(self.vreg(vreg));
|
|
||||||
|
|
||||||
// For each range in each vreg, insert moves or
|
// For each range in each vreg, insert moves or
|
||||||
// half-moves. We also scan over `blockparam_ins` and
|
// half-moves. We also scan over `blockparam_ins` and
|
||||||
// `blockparam_outs`, which are sorted by (block, vreg),
|
// `blockparam_outs`, which are sorted by (block, vreg),
|
||||||
@@ -196,17 +194,14 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
let mut prev = LiveRangeIndex::invalid();
|
let mut prev = LiveRangeIndex::invalid();
|
||||||
for range_idx in 0..self.vregs[vreg.index()].ranges.len() {
|
for range_idx in 0..self.vregs[vreg.index()].ranges.len() {
|
||||||
let entry = self.vregs[vreg.index()].ranges[range_idx];
|
let entry = self.vregs[vreg.index()].ranges[range_idx];
|
||||||
let alloc = pinned_alloc
|
let alloc = self.get_alloc_for_range(entry.index);
|
||||||
.map(|preg| Allocation::reg(preg))
|
|
||||||
.unwrap_or_else(|| self.get_alloc_for_range(entry.index));
|
|
||||||
let range = entry.range;
|
let range = entry.range;
|
||||||
trace!(
|
trace!(
|
||||||
"apply_allocations: vreg {:?} LR {:?} with range {:?} has alloc {:?} (pinned {:?})",
|
"apply_allocations: vreg {:?} LR {:?} with range {:?} has alloc {:?}",
|
||||||
vreg,
|
vreg,
|
||||||
entry.index,
|
entry.index,
|
||||||
range,
|
range,
|
||||||
alloc,
|
alloc,
|
||||||
pinned_alloc,
|
|
||||||
);
|
);
|
||||||
debug_assert!(alloc != Allocation::none());
|
debug_assert!(alloc != Allocation::none());
|
||||||
|
|
||||||
@@ -253,10 +248,7 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
// can't insert a move that logically happens just
|
// can't insert a move that logically happens just
|
||||||
// before After (i.e. in the middle of a single
|
// before After (i.e. in the middle of a single
|
||||||
// instruction).
|
// instruction).
|
||||||
//
|
if prev.is_valid() {
|
||||||
// Also note that this case is not applicable to
|
|
||||||
// pinned vregs (because they are always in one PReg).
|
|
||||||
if pinned_alloc.is_none() && prev.is_valid() {
|
|
||||||
let prev_alloc = self.get_alloc_for_range(prev);
|
let prev_alloc = self.get_alloc_for_range(prev);
|
||||||
let prev_range = self.ranges[prev.index()].range;
|
let prev_range = self.ranges[prev.index()].range;
|
||||||
let first_is_def =
|
let first_is_def =
|
||||||
@@ -286,99 +278,93 @@ impl<'a, F: Function> Env<'a, F> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The block-to-block edge-move logic is not
|
// Scan over blocks whose ends are covered by this
|
||||||
// applicable to pinned vregs, which are always in one
|
// range. For each, for each successor that is not
|
||||||
// PReg (so never need moves within their own vreg
|
// already in this range (hence guaranteed to have the
|
||||||
// ranges).
|
// same allocation) and if the vreg is live, add a
|
||||||
if pinned_alloc.is_none() {
|
// Source half-move.
|
||||||
// Scan over blocks whose ends are covered by this
|
let mut block = self.cfginfo.insn_block[range.from.inst().index()];
|
||||||
// range. For each, for each successor that is not
|
while block.is_valid() && block.index() < self.func.num_blocks() {
|
||||||
// already in this range (hence guaranteed to have the
|
if range.to < self.cfginfo.block_exit[block.index()].next() {
|
||||||
// same allocation) and if the vreg is live, add a
|
break;
|
||||||
// Source half-move.
|
}
|
||||||
let mut block = self.cfginfo.insn_block[range.from.inst().index()];
|
trace!("examining block with end in range: block{}", block.index());
|
||||||
while block.is_valid() && block.index() < self.func.num_blocks() {
|
for &succ in self.func.block_succs(block) {
|
||||||
if range.to < self.cfginfo.block_exit[block.index()].next() {
|
trace!(
|
||||||
|
" -> has succ block {} with entry {:?}",
|
||||||
|
succ.index(),
|
||||||
|
self.cfginfo.block_entry[succ.index()]
|
||||||
|
);
|
||||||
|
if range.contains_point(self.cfginfo.block_entry[succ.index()]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trace!(" -> out of this range, requires half-move if live");
|
||||||
|
if self.is_live_in(succ, vreg) {
|
||||||
|
trace!(" -> live at input to succ, adding halfmove");
|
||||||
|
half_moves.push(HalfMove {
|
||||||
|
key: half_move_key(block, succ, vreg, HalfMoveKind::Source),
|
||||||
|
alloc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan forward in `blockparam_outs`, adding all
|
||||||
|
// half-moves for outgoing values to blockparams
|
||||||
|
// in succs.
|
||||||
|
trace!(
|
||||||
|
"scanning blockparam_outs for v{} block{}: blockparam_out_idx = {}",
|
||||||
|
vreg.index(),
|
||||||
|
block.index(),
|
||||||
|
blockparam_out_idx,
|
||||||
|
);
|
||||||
|
while blockparam_out_idx < self.blockparam_outs.len() {
|
||||||
|
let BlockparamOut {
|
||||||
|
from_vreg,
|
||||||
|
from_block,
|
||||||
|
to_block,
|
||||||
|
to_vreg,
|
||||||
|
} = self.blockparam_outs[blockparam_out_idx];
|
||||||
|
if (from_vreg, from_block) > (vreg, block) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
trace!("examining block with end in range: block{}", block.index());
|
if (from_vreg, from_block) == (vreg, block) {
|
||||||
for &succ in self.func.block_succs(block) {
|
|
||||||
trace!(
|
trace!(
|
||||||
" -> has succ block {} with entry {:?}",
|
" -> found: from v{} block{} to v{} block{}",
|
||||||
succ.index(),
|
from_vreg.index(),
|
||||||
self.cfginfo.block_entry[succ.index()]
|
from_block.index(),
|
||||||
|
to_vreg.index(),
|
||||||
|
to_vreg.index()
|
||||||
);
|
);
|
||||||
if range.contains_point(self.cfginfo.block_entry[succ.index()]) {
|
half_moves.push(HalfMove {
|
||||||
continue;
|
key: half_move_key(
|
||||||
}
|
from_block,
|
||||||
trace!(" -> out of this range, requires half-move if live");
|
to_block,
|
||||||
if self.is_live_in(succ, vreg) {
|
to_vreg,
|
||||||
trace!(" -> live at input to succ, adding halfmove");
|
HalfMoveKind::Source,
|
||||||
half_moves.push(HalfMove {
|
),
|
||||||
key: half_move_key(block, succ, vreg, HalfMoveKind::Source),
|
alloc,
|
||||||
alloc,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan forward in `blockparam_outs`, adding all
|
if self.annotations_enabled {
|
||||||
// half-moves for outgoing values to blockparams
|
self.annotate(
|
||||||
// in succs.
|
self.cfginfo.block_exit[block.index()],
|
||||||
trace!(
|
format!(
|
||||||
"scanning blockparam_outs for v{} block{}: blockparam_out_idx = {}",
|
"blockparam-out: block{} to block{}: v{} to v{} in {}",
|
||||||
vreg.index(),
|
from_block.index(),
|
||||||
block.index(),
|
to_block.index(),
|
||||||
blockparam_out_idx,
|
from_vreg.index(),
|
||||||
);
|
to_vreg.index(),
|
||||||
while blockparam_out_idx < self.blockparam_outs.len() {
|
alloc
|
||||||
let BlockparamOut {
|
|
||||||
from_vreg,
|
|
||||||
from_block,
|
|
||||||
to_block,
|
|
||||||
to_vreg,
|
|
||||||
} = self.blockparam_outs[blockparam_out_idx];
|
|
||||||
if (from_vreg, from_block) > (vreg, block) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (from_vreg, from_block) == (vreg, block) {
|
|
||||||
trace!(
|
|
||||||
" -> found: from v{} block{} to v{} block{}",
|
|
||||||
from_vreg.index(),
|
|
||||||
from_block.index(),
|
|
||||||
to_vreg.index(),
|
|
||||||
to_vreg.index()
|
|
||||||
);
|
|
||||||
half_moves.push(HalfMove {
|
|
||||||
key: half_move_key(
|
|
||||||
from_block,
|
|
||||||
to_block,
|
|
||||||
to_vreg,
|
|
||||||
HalfMoveKind::Source,
|
|
||||||
),
|
),
|
||||||
alloc,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if self.annotations_enabled {
|
|
||||||
self.annotate(
|
|
||||||
self.cfginfo.block_exit[block.index()],
|
|
||||||
format!(
|
|
||||||
"blockparam-out: block{} to block{}: v{} to v{} in {}",
|
|
||||||
from_block.index(),
|
|
||||||
to_block.index(),
|
|
||||||
from_vreg.index(),
|
|
||||||
to_vreg.index(),
|
|
||||||
alloc
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockparam_out_idx += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
block = block.next();
|
blockparam_out_idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
block = block.next();
|
||||||
|
|
||||||
// Scan over blocks whose beginnings are covered by
|
// Scan over blocks whose beginnings are covered by
|
||||||
// this range and for which the vreg is live at the
|
// this range and for which the vreg is live at the
|
||||||
// start of the block. For each, for each predecessor,
|
// start of the block. For each, for each predecessor,
|
||||||
|
|||||||
17
src/lib.rs
17
src/lib.rs
@@ -1117,23 +1117,6 @@ pub trait Function {
|
|||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the given vreg pinned to a preg? If so, every use of the
|
|
||||||
/// vreg is automatically assigned to the preg, and live-ranges of
|
|
||||||
/// the vreg allocate the preg exclusively (are not spilled
|
|
||||||
/// elsewhere). The user must take care not to have too many live
|
|
||||||
/// pinned vregs such that allocation is no longer possible;
|
|
||||||
/// liverange computation will check that this is the case (that
|
|
||||||
/// there are enough remaining allocatable pregs of every class to
|
|
||||||
/// hold all Reg-constrained operands).
|
|
||||||
///
|
|
||||||
/// Pinned vregs are implicitly live-in to the function: that is,
|
|
||||||
/// one can use a pinned vreg without having first defined it, and
|
|
||||||
/// this will take the value that that physical register (to which
|
|
||||||
/// the vreg is pinned) had at function entry.
|
|
||||||
fn is_pinned_vreg(&self, _: VReg) -> Option<PReg> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
// Spills/reloads
|
// Spills/reloads
|
||||||
// --------------
|
// --------------
|
||||||
|
|||||||
Reference in New Issue
Block a user