mirror of
https://github.com/golang/go
synced 2024-11-18 18:54:42 -07:00
go.tools/ssa: expose dominator tree of control-flow graph in API.
New APIs: (*BasicBlock).{Idom,Dominees,Dominates} (*Function).DomPreorder Messy but systematic refactoring of domNode: - renamed "domInfo". - embedded directly in BasicBlock, not as pointer. Block field removed. - Level field removed; was unused. - Working state of LT algorithm now in its own type. {semi,parent,ancestor} fields moved into it. - remaining fields made private; accessors added. - use 32-bit ints for pre/postorder numbers. - allocate LT working space (5 copies of fn.Blocks) contiguously. dom.go is simpler but somewhat more verbose. Also: - we always build the domtree now---yet memory usage is down 5%. - number the Recover block too. - add sanity check for DomPreorder. R=gri CC=golang-dev https://golang.org/cl/37230043
This commit is contained in:
parent
c846ececde
commit
b5016cbbbd
232
ssa/dom.go
232
ssa/dom.go
@ -22,57 +22,90 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// domNode represents a node in the dominator tree.
|
||||
// Idom returns the block that immediately dominates b:
|
||||
// its parent in the dominator tree, if any.
|
||||
// Neither the entry node (b.Index==0) nor recover node
|
||||
// (b==b.Parent().Recover()) have a parent.
|
||||
//
|
||||
// TODO(adonovan): export this, when ready.
|
||||
type domNode struct {
|
||||
Block *BasicBlock // the basic block; n.Block.dom == n
|
||||
Idom *domNode // immediate dominator (parent in dominator tree)
|
||||
Children []*domNode // nodes dominated by this one
|
||||
Level int // level number of node within tree; zero for root
|
||||
Pre, Post int // pre- and post-order numbering within dominator tree
|
||||
func (b *BasicBlock) Idom() *BasicBlock { return b.dom.idom }
|
||||
|
||||
// Working state for Lengauer-Tarjan algorithm
|
||||
// (during which Pre is repurposed for CFG DFS preorder number).
|
||||
// TODO(adonovan): opt: measure allocating these as temps.
|
||||
semi *domNode // semidominator
|
||||
parent *domNode // parent in DFS traversal of CFG
|
||||
ancestor *domNode // ancestor with least sdom
|
||||
// Dominees returns the list of blocks that b immediately dominates:
|
||||
// its children in the dominator tree.
|
||||
//
|
||||
func (b *BasicBlock) Dominees() []*BasicBlock { return b.dom.children }
|
||||
|
||||
// Dominates reports whether b dominates c.
|
||||
func (b *BasicBlock) Dominates(c *BasicBlock) bool {
|
||||
return b.dom.pre <= c.dom.pre && c.dom.post <= b.dom.post
|
||||
}
|
||||
|
||||
// ltDfs implements the depth-first search part of the LT algorithm.
|
||||
func ltDfs(v *domNode, i int, preorder []*domNode) int {
|
||||
type byDomPreorder []*BasicBlock
|
||||
|
||||
func (a byDomPreorder) Len() int { return len(a) }
|
||||
func (a byDomPreorder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byDomPreorder) Less(i, j int) bool { return a[i].dom.pre < a[j].dom.pre }
|
||||
|
||||
// DomPreorder returns a new slice containing the blocks of f in
|
||||
// dominator tree preorder.
|
||||
//
|
||||
func (f *Function) DomPreorder() []*BasicBlock {
|
||||
n := len(f.Blocks)
|
||||
order := make(byDomPreorder, n, n)
|
||||
copy(order, f.Blocks)
|
||||
sort.Sort(order)
|
||||
return order
|
||||
}
|
||||
|
||||
// domInfo contains a BasicBlock's dominance information.
|
||||
type domInfo struct {
|
||||
idom *BasicBlock // immediate dominator (parent in domtree)
|
||||
children []*BasicBlock // nodes immediately dominated by this one
|
||||
pre, post int32 // pre- and post-order numbering within domtree
|
||||
}
|
||||
|
||||
// ltState holds the working state for Lengauer-Tarjan algorithm
|
||||
// (during which domInfo.pre is repurposed for CFG DFS preorder number).
|
||||
type ltState struct {
|
||||
// Each slice is indexed by b.Index.
|
||||
sdom []*BasicBlock // b's semidominator
|
||||
parent []*BasicBlock // b's parent in DFS traversal of CFG
|
||||
ancestor []*BasicBlock // b's ancestor with least sdom
|
||||
}
|
||||
|
||||
// dfs implements the depth-first search part of the LT algorithm.
|
||||
func (lt *ltState) dfs(v *BasicBlock, i int32, preorder []*BasicBlock) int32 {
|
||||
preorder[i] = v
|
||||
v.Pre = i // For now: DFS preorder of spanning tree of CFG
|
||||
v.dom.pre = i // For now: DFS preorder of spanning tree of CFG
|
||||
i++
|
||||
v.semi = v
|
||||
v.ancestor = nil
|
||||
for _, succ := range v.Block.Succs {
|
||||
if w := succ.dom; w.semi == nil {
|
||||
w.parent = v
|
||||
i = ltDfs(w, i, preorder)
|
||||
lt.sdom[v.Index] = v
|
||||
lt.link(nil, v)
|
||||
for _, w := range v.Succs {
|
||||
if lt.sdom[w.Index] == nil {
|
||||
lt.parent[w.Index] = v
|
||||
i = lt.dfs(w, i, preorder)
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// ltEval implements the EVAL part of the LT algorithm.
|
||||
func ltEval(v *domNode) *domNode {
|
||||
// eval implements the EVAL part of the LT algorithm.
|
||||
func (lt *ltState) eval(v *BasicBlock) *BasicBlock {
|
||||
// TODO(adonovan): opt: do path compression per simple LT.
|
||||
u := v
|
||||
for ; v.ancestor != nil; v = v.ancestor {
|
||||
if v.semi.Pre < u.semi.Pre {
|
||||
for ; lt.ancestor[v.Index] != nil; v = lt.ancestor[v.Index] {
|
||||
if lt.sdom[v.Index].dom.pre < lt.sdom[u.Index].dom.pre {
|
||||
u = v
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// ltLink implements the LINK part of the LT algorithm.
|
||||
func ltLink(v, w *domNode) {
|
||||
w.ancestor = v
|
||||
// link implements the LINK part of the LT algorithm.
|
||||
func (lt *ltState) link(v, w *BasicBlock) {
|
||||
lt.ancestor[w.Index] = v
|
||||
}
|
||||
|
||||
// buildDomTree computes the dominator tree of f using the LT algorithm.
|
||||
@ -82,90 +115,89 @@ func buildDomTree(f *Function) {
|
||||
// The step numbers refer to the original LT paper; the
|
||||
// reodering is due to Georgiadis.
|
||||
|
||||
// Initialize domNode nodes.
|
||||
// Clear any previous domInfo.
|
||||
for _, b := range f.Blocks {
|
||||
dom := b.dom
|
||||
if dom == nil {
|
||||
dom = &domNode{Block: b}
|
||||
b.dom = dom
|
||||
} else {
|
||||
dom.Block = b // reuse
|
||||
}
|
||||
b.dom = domInfo{}
|
||||
}
|
||||
|
||||
n := len(f.Blocks)
|
||||
// Allocate space for 5 contiguous [n]*BasicBlock arrays:
|
||||
// sdom, parent, ancestor, preorder, buckets.
|
||||
space := make([]*BasicBlock, 5*n, 5*n)
|
||||
lt := ltState{
|
||||
sdom: space[0:n],
|
||||
parent: space[n : 2*n],
|
||||
ancestor: space[2*n : 3*n],
|
||||
}
|
||||
|
||||
// Step 1. Number vertices by depth-first preorder.
|
||||
n := len(f.Blocks)
|
||||
preorder := make([]*domNode, n)
|
||||
root := f.Blocks[0].dom
|
||||
prenum := ltDfs(root, 0, preorder)
|
||||
var recover *domNode
|
||||
if f.Recover != nil {
|
||||
recover = f.Recover.dom
|
||||
ltDfs(recover, prenum, preorder)
|
||||
preorder := space[3*n : 4*n]
|
||||
root := f.Blocks[0]
|
||||
prenum := lt.dfs(root, 0, preorder)
|
||||
recover := f.Recover
|
||||
if recover != nil {
|
||||
lt.dfs(recover, prenum, preorder)
|
||||
}
|
||||
|
||||
buckets := make([]*domNode, n)
|
||||
buckets := space[4*n : 5*n]
|
||||
copy(buckets, preorder)
|
||||
|
||||
// In reverse preorder...
|
||||
for i := n - 1; i > 0; i-- {
|
||||
for i := int32(n) - 1; i > 0; i-- {
|
||||
w := preorder[i]
|
||||
|
||||
// Step 3. Implicitly define the immediate dominator of each node.
|
||||
for v := buckets[i]; v != w; v = buckets[v.Pre] {
|
||||
u := ltEval(v)
|
||||
if u.semi.Pre < i {
|
||||
v.Idom = u
|
||||
for v := buckets[i]; v != w; v = buckets[v.dom.pre] {
|
||||
u := lt.eval(v)
|
||||
if lt.sdom[u.Index].dom.pre < i {
|
||||
v.dom.idom = u
|
||||
} else {
|
||||
v.Idom = w
|
||||
v.dom.idom = w
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2. Compute the semidominators of all nodes.
|
||||
w.semi = w.parent
|
||||
for _, pred := range w.Block.Preds {
|
||||
v := pred.dom
|
||||
u := ltEval(v)
|
||||
if u.semi.Pre < w.semi.Pre {
|
||||
w.semi = u.semi
|
||||
lt.sdom[w.Index] = lt.parent[w.Index]
|
||||
for _, v := range w.Preds {
|
||||
u := lt.eval(v)
|
||||
if lt.sdom[u.Index].dom.pre < lt.sdom[w.Index].dom.pre {
|
||||
lt.sdom[w.Index] = lt.sdom[u.Index]
|
||||
}
|
||||
}
|
||||
|
||||
ltLink(w.parent, w)
|
||||
lt.link(lt.parent[w.Index], w)
|
||||
|
||||
if w.parent == w.semi {
|
||||
w.Idom = w.parent
|
||||
if lt.parent[w.Index] == lt.sdom[w.Index] {
|
||||
w.dom.idom = lt.parent[w.Index]
|
||||
} else {
|
||||
buckets[i] = buckets[w.semi.Pre]
|
||||
buckets[w.semi.Pre] = w
|
||||
buckets[i] = buckets[lt.sdom[w.Index].dom.pre]
|
||||
buckets[lt.sdom[w.Index].dom.pre] = w
|
||||
}
|
||||
}
|
||||
|
||||
// The final 'Step 3' is now outside the loop.
|
||||
for v := buckets[0]; v != root; v = buckets[v.Pre] {
|
||||
v.Idom = root
|
||||
for v := buckets[0]; v != root; v = buckets[v.dom.pre] {
|
||||
v.dom.idom = root
|
||||
}
|
||||
|
||||
// Step 4. Explicitly define the immediate dominator of each
|
||||
// node, in preorder.
|
||||
for _, w := range preorder[1:] {
|
||||
if w == root || w == recover {
|
||||
w.Idom = nil
|
||||
w.dom.idom = nil
|
||||
} else {
|
||||
if w.Idom != w.semi {
|
||||
w.Idom = w.Idom.Idom
|
||||
if w.dom.idom != lt.sdom[w.Index] {
|
||||
w.dom.idom = w.dom.idom.dom.idom
|
||||
}
|
||||
// Calculate Children relation as inverse of Idom.
|
||||
w.Idom.Children = append(w.Idom.Children, w)
|
||||
w.dom.idom.dom.children = append(w.dom.idom.dom.children, w)
|
||||
}
|
||||
|
||||
// Clear working state.
|
||||
w.semi = nil
|
||||
w.parent = nil
|
||||
w.ancestor = nil
|
||||
}
|
||||
|
||||
numberDomTree(root, 0, 0, 0)
|
||||
pre, post := numberDomTree(root, 0, 0)
|
||||
if recover != nil {
|
||||
numberDomTree(recover, pre, post)
|
||||
}
|
||||
|
||||
// printDomTreeDot(os.Stderr, f) // debugging
|
||||
// printDomTreeText(os.Stderr, root, 0) // debugging
|
||||
@ -177,29 +209,19 @@ func buildDomTree(f *Function) {
|
||||
|
||||
// numberDomTree sets the pre- and post-order numbers of a depth-first
|
||||
// traversal of the dominator tree rooted at v. These are used to
|
||||
// answer dominance queries in constant time. Also, it sets the level
|
||||
// numbers (zero for the root) used for frontier computation.
|
||||
// answer dominance queries in constant time.
|
||||
//
|
||||
func numberDomTree(v *domNode, pre, post, level int) (int, int) {
|
||||
v.Level = level
|
||||
level++
|
||||
v.Pre = pre
|
||||
func numberDomTree(v *BasicBlock, pre, post int32) (int32, int32) {
|
||||
v.dom.pre = pre
|
||||
pre++
|
||||
for _, child := range v.Children {
|
||||
pre, post = numberDomTree(child, pre, post, level)
|
||||
for _, child := range v.dom.children {
|
||||
pre, post = numberDomTree(child, pre, post)
|
||||
}
|
||||
v.Post = post
|
||||
v.dom.post = post
|
||||
post++
|
||||
return pre, post
|
||||
}
|
||||
|
||||
// dominates returns true if b dominates c.
|
||||
// Requires that dominance information is up-to-date.
|
||||
//
|
||||
func dominates(b, c *BasicBlock) bool {
|
||||
return b.dom.Pre <= c.dom.Pre && c.dom.Post <= b.dom.Post
|
||||
}
|
||||
|
||||
// Testing utilities ----------------------------------------
|
||||
|
||||
// sanityCheckDomTree checks the correctness of the dominator tree
|
||||
@ -223,7 +245,7 @@ func sanityCheckDomTree(f *Function) {
|
||||
// Initialization.
|
||||
for i, b := range f.Blocks {
|
||||
if i == 0 || b == f.Recover {
|
||||
// The root is dominated only by itself.
|
||||
// A root is dominated only by itself.
|
||||
D[i].SetBit(&D[0], 0, 1)
|
||||
} else {
|
||||
// All other blocks are (initially) dominated
|
||||
@ -262,7 +284,7 @@ func sanityCheckDomTree(f *Function) {
|
||||
if c == f.Recover {
|
||||
continue
|
||||
}
|
||||
actual := dominates(b, c)
|
||||
actual := b.Dominates(c)
|
||||
expected := D[j].Bit(i) == 1
|
||||
if actual != expected {
|
||||
fmt.Fprintf(os.Stderr, "dominates(%s, %s)==%t, want %t\n", b, c, actual, expected)
|
||||
@ -270,17 +292,27 @@ func sanityCheckDomTree(f *Function) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preorder := f.DomPreorder()
|
||||
for _, b := range f.Blocks {
|
||||
if got := preorder[b.dom.pre]; got != b {
|
||||
fmt.Fprintf(os.Stderr, "preorder[%d]==%s, want %s\n", b.dom.pre, got, b)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
panic("sanityCheckDomTree failed for " + f.String())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Printing functions ----------------------------------------
|
||||
|
||||
// printDomTree prints the dominator tree as text, using indentation.
|
||||
func printDomTreeText(w io.Writer, v *domNode, indent int) {
|
||||
fmt.Fprintf(w, "%*s%s\n", 4*indent, "", v.Block)
|
||||
for _, child := range v.Children {
|
||||
func printDomTreeText(w io.Writer, v *BasicBlock, indent int) {
|
||||
fmt.Fprintf(w, "%*s%s\n", 4*indent, "", v)
|
||||
for _, child := range v.dom.children {
|
||||
printDomTreeText(w, child, indent+1)
|
||||
}
|
||||
}
|
||||
@ -292,17 +324,17 @@ func printDomTreeDot(w io.Writer, f *Function) {
|
||||
fmt.Fprintln(w, "digraph domtree {")
|
||||
for i, b := range f.Blocks {
|
||||
v := b.dom
|
||||
fmt.Fprintf(w, "\tn%d [label=\"%s (%d, %d)\",shape=\"rectangle\"];\n", v.Pre, b, v.Pre, v.Post)
|
||||
fmt.Fprintf(w, "\tn%d [label=\"%s (%d, %d)\",shape=\"rectangle\"];\n", v.pre, b, v.pre, v.post)
|
||||
// TODO(adonovan): improve appearance of edges
|
||||
// belonging to both dominator tree and CFG.
|
||||
|
||||
// Dominator tree edge.
|
||||
if i != 0 {
|
||||
fmt.Fprintf(w, "\tn%d -> n%d [style=\"solid\",weight=100];\n", v.Idom.Pre, v.Pre)
|
||||
fmt.Fprintf(w, "\tn%d -> n%d [style=\"solid\",weight=100];\n", v.idom.dom.pre, v.pre)
|
||||
}
|
||||
// CFG edges.
|
||||
for _, pred := range b.Preds {
|
||||
fmt.Fprintf(w, "\tn%d -> n%d [style=\"dotted\",weight=0];\n", pred.dom.Pre, v.Pre)
|
||||
fmt.Fprintf(w, "\tn%d -> n%d [style=\"dotted\",weight=0];\n", pred.dom.pre, v.pre)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, "}")
|
||||
|
@ -324,11 +324,12 @@ func (f *Function) finishBody() {
|
||||
|
||||
buildReferrers(f)
|
||||
|
||||
buildDomTree(f)
|
||||
|
||||
if f.Prog.mode&NaiveForm == 0 {
|
||||
// For debugging pre-state of lifting pass:
|
||||
// numberRegisters(f)
|
||||
// f.DumpTo(os.Stderr)
|
||||
|
||||
lift(f)
|
||||
}
|
||||
|
||||
|
38
ssa/lift.go
38
ssa/lift.go
@ -68,9 +68,9 @@ const debugLifting = false
|
||||
//
|
||||
type domFrontier [][]*BasicBlock
|
||||
|
||||
func (df domFrontier) add(u, v *domNode) {
|
||||
p := &df[u.Block.Index]
|
||||
*p = append(*p, v.Block)
|
||||
func (df domFrontier) add(u, v *BasicBlock) {
|
||||
p := &df[u.Index]
|
||||
*p = append(*p, v)
|
||||
}
|
||||
|
||||
// build builds the dominance frontier df for the dominator (sub)tree
|
||||
@ -79,21 +79,21 @@ func (df domFrontier) add(u, v *domNode) {
|
||||
// TODO(adonovan): opt: consider Berlin approach, computing pruned SSA
|
||||
// by pruning the entire IDF computation, rather than merely pruning
|
||||
// the DF -> IDF step.
|
||||
func (df domFrontier) build(u *domNode) {
|
||||
func (df domFrontier) build(u *BasicBlock) {
|
||||
// Encounter each node u in postorder of dom tree.
|
||||
for _, child := range u.Children {
|
||||
for _, child := range u.dom.children {
|
||||
df.build(child)
|
||||
}
|
||||
for _, vb := range u.Block.Succs {
|
||||
if v := vb.dom; v.Idom != u {
|
||||
df.add(u, v)
|
||||
for _, vb := range u.Succs {
|
||||
if v := vb.dom; v.idom != u {
|
||||
df.add(u, vb)
|
||||
}
|
||||
}
|
||||
for _, w := range u.Children {
|
||||
for _, vb := range df[w.Block.Index] {
|
||||
for _, w := range u.dom.children {
|
||||
for _, vb := range df[w.Index] {
|
||||
// TODO(adonovan): opt: use word-parallel bitwise union.
|
||||
if v := vb.dom; v.Idom != u {
|
||||
df.add(u, v)
|
||||
if v := vb.dom; v.idom != u {
|
||||
df.add(u, vb)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,9 +101,9 @@ func (df domFrontier) build(u *domNode) {
|
||||
|
||||
func buildDomFrontier(fn *Function) domFrontier {
|
||||
df := make(domFrontier, len(fn.Blocks))
|
||||
df.build(fn.Blocks[0].dom)
|
||||
df.build(fn.Blocks[0])
|
||||
if fn.Recover != nil {
|
||||
df.build(fn.Recover.dom)
|
||||
df.build(fn.Recover)
|
||||
}
|
||||
return df
|
||||
}
|
||||
@ -115,11 +115,12 @@ func buildDomFrontier(fn *Function) domFrontier {
|
||||
// Preconditions:
|
||||
// - fn has no dead blocks (blockopt has run).
|
||||
// - Def/use info (Operands and Referrers) is up-to-date.
|
||||
// - The dominator tree is up-to-date.
|
||||
//
|
||||
func lift(fn *Function) {
|
||||
// TODO(adonovan): opt: lots of little optimizations may be
|
||||
// worthwhile here, especially if they cause us to avoid
|
||||
// buildDomTree. For example:
|
||||
// buildDomFrontier. For example:
|
||||
//
|
||||
// - Alloc never loaded? Eliminate.
|
||||
// - Alloc never stored? Replace all loads with a zero constant.
|
||||
@ -135,9 +136,6 @@ func lift(fn *Function) {
|
||||
// Unclear.
|
||||
//
|
||||
// But we will start with the simplest correct code.
|
||||
|
||||
buildDomTree(fn)
|
||||
|
||||
df := buildDomFrontier(fn)
|
||||
|
||||
if debugLifting {
|
||||
@ -562,10 +560,10 @@ func rename(u *BasicBlock, renaming []Value, newPhis newPhiMap) {
|
||||
|
||||
// Continue depth-first recursion over domtree, pushing a
|
||||
// fresh copy of the renaming map for each subtree.
|
||||
for _, v := range u.dom.Children {
|
||||
for _, v := range u.dom.children {
|
||||
// TODO(adonovan): opt: avoid copy on final iteration; use destructive update.
|
||||
r := make([]Value, len(renaming))
|
||||
copy(r, renaming)
|
||||
rename(v.Block, r, newPhis)
|
||||
rename(v, r, newPhis)
|
||||
}
|
||||
}
|
||||
|
13
ssa/ssa.go
13
ssa/ssa.go
@ -239,9 +239,11 @@ type Instruction interface {
|
||||
//
|
||||
// Functions are immutable values; they do not have addresses.
|
||||
//
|
||||
// Blocks contains the function's control-flow graph (CFG).
|
||||
// Blocks[0] is the function entry point; block order is not otherwise
|
||||
// semantically significant, though it may affect the readability of
|
||||
// the disassembly.
|
||||
// To iterate over the blocks in dominance order, use DomPreorder().
|
||||
//
|
||||
// Recover is an optional second entry point to which control resumes
|
||||
// after a recovered panic. The Recover block may contain only a load
|
||||
@ -302,8 +304,13 @@ type Function struct {
|
||||
// i.e. Preds is nil. Empty blocks are typically pruned.
|
||||
//
|
||||
// BasicBlocks and their Preds/Succs relation form a (possibly cyclic)
|
||||
// graph independent of the SSA Value graph. It is illegal for
|
||||
// multiple edges to exist between the same pair of blocks.
|
||||
// graph independent of the SSA Value graph: the control-flow graph or
|
||||
// CFG. It is illegal for multiple edges to exist between the same
|
||||
// pair of blocks.
|
||||
//
|
||||
// Each BasicBlock is also a node in the dominator tree of the CFG.
|
||||
// The tree may be navigated using Idom()/Dominees() and queried using
|
||||
// Dominates().
|
||||
//
|
||||
// The order of Preds and Succs are significant (to Phi and If
|
||||
// instructions, respectively).
|
||||
@ -315,7 +322,7 @@ type BasicBlock struct {
|
||||
Instrs []Instruction // instructions in order
|
||||
Preds, Succs []*BasicBlock // predecessors and successors
|
||||
succs2 [2]*BasicBlock // initial space for Succs.
|
||||
dom *domNode // node in dominator tree; optional.
|
||||
dom domInfo // dominator tree info
|
||||
gaps int // number of nil Instrs (transient).
|
||||
rundefers int // number of rundefers (transient)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user