mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 08:14:09 -08:00
C backend: fix more bugs, pass more tests
This commit is contained in:
parent
899725b6bd
commit
d2aa37d6a4
|
@ -767,12 +767,12 @@ public class ClassGenerator {
|
|||
|
||||
int lower = ranges.get(0).lower;
|
||||
int upper = ranges.get(ranges.size() - 1).upper;
|
||||
isSupertypeWriter.println("if (tag < " + lower + " || tag > " + upper + ") return INT32_C(0);");
|
||||
isSupertypeWriter.println("if (tag < " + lower + " || tag >= " + upper + ") return INT32_C(0);");
|
||||
|
||||
for (int i = 1; i < ranges.size(); ++i) {
|
||||
lower = ranges.get(i - 1).upper;
|
||||
upper = ranges.get(i).lower;
|
||||
isSupertypeWriter.println("if (tag < " + lower + " || tag > " + upper + ") return INT32_C(0);");
|
||||
isSupertypeWriter.println("if (tag >= " + lower + " || tag < " + upper + ") return INT32_C(0);");
|
||||
}
|
||||
|
||||
isSupertypeWriter.println("return INT32_C(1);");
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package org.teavm.model.lowlevel;
|
||||
|
||||
import com.carrotsearch.hppc.IntHashSet;
|
||||
import com.carrotsearch.hppc.IntSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
@ -134,6 +136,9 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
int[] currentJointSources = new int[program.variableCount()];
|
||||
int[] jointReceiverMap = new int[program.variableCount()];
|
||||
Arrays.fill(currentJointSources, -1);
|
||||
Arrays.fill(jointReceiverMap, -1);
|
||||
IntSet outgoingVariablesToRemove = new IntHashSet();
|
||||
IntSet variablesDefinedHere = new IntHashSet();
|
||||
|
||||
for (TryCatchBlock tryCatch : block.getTryCatchBlocks()) {
|
||||
for (Phi phi : tryCatch.getHandler().getPhis()) {
|
||||
|
@ -162,6 +167,7 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
DefinitionExtractor defExtractor = new DefinitionExtractor();
|
||||
List<BasicBlock> blocksToClearHandlers = new ArrayList<>();
|
||||
blocksToClearHandlers.add(block);
|
||||
BasicBlock initialBlock = block;
|
||||
|
||||
for (Instruction insn : block) {
|
||||
if (isCallInstruction(insn)) {
|
||||
|
@ -210,7 +216,8 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
CallSiteDescriptor callSite = new CallSiteDescriptor(callSites.size(), location);
|
||||
callSites.add(callSite);
|
||||
List<Instruction> pre = setLocation(getInstructionsBeforeCallSite(callSite), insn.getLocation());
|
||||
List<Instruction> post = getInstructionsAfterCallSite(block, next, callSite, currentJointSources);
|
||||
List<Instruction> post = getInstructionsAfterCallSite(initialBlock, block, next, callSite,
|
||||
currentJointSources, outgoingVariablesToRemove, variablesDefinedHere);
|
||||
post = setLocation(post, insn.getLocation());
|
||||
block.getLastInstruction().insertPreviousAll(pre);
|
||||
block.addAll(post);
|
||||
|
@ -220,15 +227,27 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
break;
|
||||
}
|
||||
block = next;
|
||||
outgoingVariablesToRemove.clear();
|
||||
variablesDefinedHere.clear();
|
||||
}
|
||||
|
||||
insn.acceptVisitor(defExtractor);
|
||||
for (Variable definedVar : defExtractor.getDefinedVariables()) {
|
||||
int jointReceiver = jointReceiverMap[definedVar.getIndex()];
|
||||
currentJointSources[jointReceiver] = definedVar.getIndex();
|
||||
if (jointReceiver >= 0) {
|
||||
int formerVar = currentJointSources[jointReceiver];
|
||||
if (formerVar >= 0) {
|
||||
if (variableDefinitionPlaces[formerVar] == initialBlock) {
|
||||
outgoingVariablesToRemove.add(formerVar);
|
||||
}
|
||||
}
|
||||
currentJointSources[jointReceiver] = definedVar.getIndex();
|
||||
variablesDefinedHere.add(definedVar.getIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixOutgoingPhis(initialBlock, block, currentJointSources, outgoingVariablesToRemove, variablesDefinedHere);
|
||||
for (BasicBlock blockToClear : blocksToClearHandlers) {
|
||||
blockToClear.getTryCatchBlocks().clear();
|
||||
}
|
||||
|
@ -287,8 +306,9 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
return instructions;
|
||||
}
|
||||
|
||||
private List<Instruction> getInstructionsAfterCallSite(BasicBlock block, BasicBlock next,
|
||||
CallSiteDescriptor callSite, int[] currentJointSources) {
|
||||
private List<Instruction> getInstructionsAfterCallSite(BasicBlock initialBlock, BasicBlock block, BasicBlock next,
|
||||
CallSiteDescriptor callSite, int[] currentJointSources, IntSet outgoingVariablesToRemove,
|
||||
IntSet variablesDefinedHere) {
|
||||
Program program = block.getProgram();
|
||||
List<Instruction> instructions = new ArrayList<>();
|
||||
|
||||
|
@ -325,20 +345,8 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
catchEntry.setCondition(handler.getId());
|
||||
switchInsn.getEntries().add(catchEntry);
|
||||
}
|
||||
|
||||
for (Phi phi : tryCatch.getHandler().getPhis()) {
|
||||
int value = currentJointSources[phi.getReceiver().getIndex()];
|
||||
if (value < 0) {
|
||||
continue;
|
||||
}
|
||||
for (Incoming incoming : phi.getIncomings()) {
|
||||
if (incoming.getValue().getIndex() == value) {
|
||||
incoming.setSource(block);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fixOutgoingPhis(initialBlock, block, currentJointSources, outgoingVariablesToRemove, variablesDefinedHere);
|
||||
|
||||
if (!defaultExists) {
|
||||
switchInsn.setDefaultTarget(getDefaultExceptionHandler());
|
||||
|
@ -370,6 +378,41 @@ public class ExceptionHandlingShadowStackContributor {
|
|||
return instructions;
|
||||
}
|
||||
|
||||
private void fixOutgoingPhis(BasicBlock block, BasicBlock newBlock, int[] currentJointSources,
|
||||
IntSet outgoingVariablesToRemove, IntSet variablesDefinedHere) {
|
||||
for (TryCatchBlock tryCatch : block.getTryCatchBlocks()) {
|
||||
for (Phi phi : tryCatch.getHandler().getPhis()) {
|
||||
int value = currentJointSources[phi.getReceiver().getIndex()];
|
||||
if (value < 0) {
|
||||
continue;
|
||||
}
|
||||
List<Incoming> additionalIncomings = new ArrayList<>();
|
||||
for (int i = 0; i < phi.getIncomings().size(); i++) {
|
||||
Incoming incoming = phi.getIncomings().get(i);
|
||||
if (incoming.getSource() != block) {
|
||||
continue;
|
||||
}
|
||||
if (outgoingVariablesToRemove.contains(incoming.getValue().getIndex())) {
|
||||
phi.getIncomings().remove(i--);
|
||||
break;
|
||||
} else if (incoming.getValue().getIndex() == value) {
|
||||
if (variablesDefinedHere.contains(value)) {
|
||||
incoming.setSource(newBlock);
|
||||
} else {
|
||||
Incoming incomingCopy = new Incoming();
|
||||
incomingCopy.setSource(newBlock);
|
||||
incomingCopy.setValue(incoming.getValue());
|
||||
additionalIncomings.add(incomingCopy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
phi.getIncomings().addAll(additionalIncomings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BasicBlock getDefaultExceptionHandler() {
|
||||
if (defaultExceptionHandler == null) {
|
||||
defaultExceptionHandler = program.createBasicBlock();
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
package org.teavm.model.lowlevel;
|
||||
|
||||
import org.teavm.model.BasicBlock;
|
||||
import org.teavm.model.Incoming;
|
||||
import org.teavm.model.Instruction;
|
||||
import org.teavm.model.MethodReference;
|
||||
import org.teavm.model.Phi;
|
||||
import org.teavm.model.Program;
|
||||
import org.teavm.model.ValueType;
|
||||
import org.teavm.model.Variable;
|
||||
|
@ -36,46 +34,29 @@ import org.teavm.model.instructions.JumpInstruction;
|
|||
import org.teavm.model.instructions.LongConstantInstruction;
|
||||
import org.teavm.model.instructions.NullCheckInstruction;
|
||||
import org.teavm.model.instructions.NullConstantInstruction;
|
||||
import org.teavm.model.optimization.RedundantJumpElimination;
|
||||
import org.teavm.model.util.ProgramUtils;
|
||||
import org.teavm.model.util.BasicBlockSplitter;
|
||||
import org.teavm.runtime.ExceptionHandling;
|
||||
|
||||
public class NullCheckTransformation {
|
||||
public void apply(Program program, ValueType returnType) {
|
||||
int[] mappings = new int[program.basicBlockCount()];
|
||||
for (int i = 0; i < mappings.length; ++i) {
|
||||
mappings[i] = i;
|
||||
}
|
||||
BasicBlockSplitter splitter = new BasicBlockSplitter(program);
|
||||
|
||||
BasicBlock returnBlock = null;
|
||||
|
||||
int count = program.basicBlockCount();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
BasicBlock next = program.basicBlockAt(i);
|
||||
BasicBlock block = null;
|
||||
int newIndex = i;
|
||||
BasicBlock block;
|
||||
while (next != null) {
|
||||
block = next;
|
||||
newIndex = block.getIndex();
|
||||
next = null;
|
||||
for (Instruction instruction : block) {
|
||||
if (!(instruction instanceof NullCheckInstruction)) {
|
||||
continue;
|
||||
}
|
||||
NullCheckInstruction nullCheck = (NullCheckInstruction) instruction;
|
||||
|
||||
BasicBlock continueBlock = program.createBasicBlock();
|
||||
|
||||
continueBlock.getTryCatchBlocks().addAll(ProgramUtils.copyTryCatches(block, program));
|
||||
while (nullCheck.getNext() != null) {
|
||||
Instruction nextInstruction = nullCheck.getNext();
|
||||
nextInstruction.delete();
|
||||
continueBlock.add(nextInstruction);
|
||||
}
|
||||
|
||||
BasicBlock continueBlock = splitter.split(block, nullCheck);
|
||||
BasicBlock throwBlock = program.createBasicBlock();
|
||||
|
||||
throwBlock.getTryCatchBlocks().addAll(ProgramUtils.copyTryCatches(block, program));
|
||||
InvokeInstruction throwNPE = new InvokeInstruction();
|
||||
throwNPE.setType(InvocationType.SPECIAL);
|
||||
throwNPE.setMethod(new MethodReference(ExceptionHandling.class, "throwNullPointerException",
|
||||
|
@ -109,8 +90,6 @@ public class NullCheckTransformation {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mappings[i] = newIndex;
|
||||
}
|
||||
|
||||
if (returnBlock != null) {
|
||||
|
@ -123,18 +102,7 @@ public class NullCheckTransformation {
|
|||
returnBlock.add(fakeExit);
|
||||
}
|
||||
|
||||
for (BasicBlock block : program.getBasicBlocks()) {
|
||||
for (Phi phi : block.getPhis()) {
|
||||
for (Incoming incoming : phi.getIncomings()) {
|
||||
int source = incoming.getSource().getIndex();
|
||||
if (source < mappings.length && mappings[source] != source) {
|
||||
incoming.setSource(program.basicBlockAt(mappings[source]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RedundantJumpElimination.optimize(program);
|
||||
splitter.fixProgram();
|
||||
}
|
||||
|
||||
private void createFakeReturnValue(BasicBlock block, Variable variable, ValueType type) {
|
||||
|
|
|
@ -38,6 +38,7 @@ public class ListingBuilder {
|
|||
}
|
||||
sb.append(prefix).append("var @").append(stringifier.getVariableLabel(i));
|
||||
sb.append(" as ").append(var.getDebugName());
|
||||
sb.append(" // " + var.getIndex());
|
||||
sb.append('\n');
|
||||
}
|
||||
for (int i = 0; i < program.basicBlockCount(); ++i) {
|
||||
|
|
188
core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java
Normal file
188
core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright 2018 Alexey Andreev.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.teavm.model.util;
|
||||
|
||||
import com.carrotsearch.hppc.ByteArrayList;
|
||||
import com.carrotsearch.hppc.ByteIndexedContainer;
|
||||
import com.carrotsearch.hppc.IntArrayList;
|
||||
import com.carrotsearch.hppc.IntIndexedContainer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.teavm.model.BasicBlock;
|
||||
import org.teavm.model.Incoming;
|
||||
import org.teavm.model.Instruction;
|
||||
import org.teavm.model.Phi;
|
||||
import org.teavm.model.Program;
|
||||
import org.teavm.model.Variable;
|
||||
import org.teavm.model.optimization.RedundantJumpElimination;
|
||||
|
||||
public class BasicBlockSplitter {
|
||||
private Program program;
|
||||
private int[] mappings;
|
||||
private IntIndexedContainer previousPtr;
|
||||
private IntIndexedContainer firstPtr;
|
||||
private ByteIndexedContainer isLastInSequence;
|
||||
private BasicBlock[] variableDefinedAt;
|
||||
|
||||
public BasicBlockSplitter(Program program) {
|
||||
this.program = program;
|
||||
}
|
||||
|
||||
private void initIfNecessary() {
|
||||
if (mappings != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mappings = new int[program.basicBlockCount()];
|
||||
previousPtr = new IntArrayList(program.basicBlockCount() * 2);
|
||||
firstPtr = new IntArrayList(program.basicBlockCount() * 2);
|
||||
isLastInSequence = new ByteArrayList(program.basicBlockCount() * 2);
|
||||
for (int i = 0; i < mappings.length; ++i) {
|
||||
mappings[i] = i;
|
||||
previousPtr.add(i);
|
||||
firstPtr.add(i);
|
||||
isLastInSequence.add((byte) 1);
|
||||
}
|
||||
|
||||
variableDefinedAt = ProgramUtils.getVariableDefinitionPlaces(program);
|
||||
}
|
||||
|
||||
public BasicBlock split(BasicBlock block, Instruction afterInstruction) {
|
||||
initIfNecessary();
|
||||
|
||||
if (afterInstruction.getBasicBlock() != block) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
if (isLastInSequence.get(block.getIndex()) == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
BasicBlock splitBlock = program.createBasicBlock();
|
||||
while (previousPtr.size() < splitBlock.getIndex()) {
|
||||
previousPtr.add(previousPtr.size());
|
||||
firstPtr.add(firstPtr.size());
|
||||
isLastInSequence.add((byte) 1);
|
||||
}
|
||||
isLastInSequence.set(block.getIndex(), (byte) 0);
|
||||
previousPtr.add(block.getIndex());
|
||||
firstPtr.add(firstPtr.get(block.getIndex()));
|
||||
mappings[firstPtr.get(block.getIndex())] = splitBlock.getIndex();
|
||||
isLastInSequence.add((byte) 1);
|
||||
|
||||
splitBlock.getTryCatchBlocks().addAll(ProgramUtils.copyTryCatches(block, program));
|
||||
while (afterInstruction.getNext() != null) {
|
||||
Instruction nextInstruction = afterInstruction.getNext();
|
||||
nextInstruction.delete();
|
||||
splitBlock.add(nextInstruction);
|
||||
}
|
||||
|
||||
return splitBlock;
|
||||
}
|
||||
|
||||
public void fixProgram() {
|
||||
if (mappings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (BasicBlock block : program.getBasicBlocks()) {
|
||||
Map<BasicBlock, List<Incoming>> incomingsBySource = new LinkedHashMap<>();
|
||||
for (Phi phi : block.getPhis()) {
|
||||
for (Incoming incoming : phi.getIncomings()) {
|
||||
if (mappings[incoming.getSource().getIndex()] == incoming.getSource().getIndex()) {
|
||||
continue;
|
||||
}
|
||||
incomingsBySource.computeIfAbsent(incoming.getSource(), b -> new ArrayList<>()).add(incoming);
|
||||
}
|
||||
}
|
||||
|
||||
for (BasicBlock source : incomingsBySource.keySet()) {
|
||||
boolean isExceptionHandler = source.getTryCatchBlocks().stream()
|
||||
.anyMatch(tryCatch -> tryCatch.getHandler() == block);
|
||||
if (isExceptionHandler) {
|
||||
fixIncomingsInExceptionHandler(source, incomingsBySource.get(source));
|
||||
} else {
|
||||
BasicBlock newSource = program.basicBlockAt(mappings[source.getIndex()]);
|
||||
for (Incoming incoming : incomingsBySource.get(source)) {
|
||||
incoming.setSource(newSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RedundantJumpElimination.optimize(program);
|
||||
}
|
||||
|
||||
private void fixIncomingsInExceptionHandler(BasicBlock source, List<Incoming> incomings) {
|
||||
List<BasicBlock> sourceParts = buildBasicBlocksSequence(source);
|
||||
assert sourceParts.get(0) == source;
|
||||
Map<Variable, List<Incoming>> incomingsByValue = groupIncomingsByValue(incomings);
|
||||
Map<Phi, Variable> lastDefinedValues = new HashMap<>();
|
||||
|
||||
for (Incoming incoming : incomings) {
|
||||
if (variableDefinedAt[incoming.getValue().getIndex()] != source) {
|
||||
lastDefinedValues.put(incoming.getPhi(), incoming.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
DefinitionExtractor defExtractor = new DefinitionExtractor();
|
||||
for (BasicBlock block : sourceParts) {
|
||||
if (block != null) {
|
||||
for (Map.Entry<Phi, Variable> lastDefinedEntry : lastDefinedValues.entrySet()) {
|
||||
Incoming incomingCopy = new Incoming();
|
||||
incomingCopy.setSource(block);
|
||||
incomingCopy.setValue(lastDefinedEntry.getValue());
|
||||
lastDefinedEntry.getKey().getIncomings().add(incomingCopy);
|
||||
}
|
||||
}
|
||||
|
||||
List<Variable> definedVars = ProgramUtils.getVariablesDefinedInBlock(block, defExtractor);
|
||||
for (Variable definedVar : definedVars) {
|
||||
List<Incoming> incomingsOfDefinedVar = incomingsByValue.get(definedVar);
|
||||
if (incomingsOfDefinedVar != null) {
|
||||
for (Incoming incoming : incomingsOfDefinedVar) {
|
||||
incoming.setSource(block);
|
||||
lastDefinedValues.put(incoming.getPhi(), definedVar);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<BasicBlock> buildBasicBlocksSequence(BasicBlock first) {
|
||||
List<BasicBlock> result = new ArrayList<>(2);
|
||||
BasicBlock block = program.basicBlockAt(mappings[first.getIndex()]);
|
||||
while (previousPtr.get(block.getIndex()) != block.getIndex()) {
|
||||
result.add(block);
|
||||
block = program.basicBlockAt(previousPtr.get(block.getIndex()));
|
||||
}
|
||||
result.add(block);
|
||||
Collections.reverse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Variable, List<Incoming>> groupIncomingsByValue(List<Incoming> incomings) {
|
||||
Map<Variable, List<Incoming>> incoingsByValue = new HashMap<>();
|
||||
for (Incoming incoming : incomings) {
|
||||
incoingsByValue.computeIfAbsent(incoming.getValue(), i -> new ArrayList<>()).add(incoming);
|
||||
}
|
||||
return incoingsByValue;
|
||||
}
|
||||
}
|
|
@ -228,4 +228,22 @@ public final class ProgramUtils {
|
|||
|
||||
return Arrays.asList(newNPE, initNPE, raise);
|
||||
}
|
||||
|
||||
public static List<Variable> getVariablesDefinedInBlock(BasicBlock block, DefinitionExtractor defExtractor) {
|
||||
List<Variable> varsDefinedInBlock = new ArrayList<>();
|
||||
for (Phi phi : block.getPhis()) {
|
||||
varsDefinedInBlock.add(phi.getReceiver());
|
||||
}
|
||||
if (block.getExceptionVariable() != null) {
|
||||
varsDefinedInBlock.add(block.getExceptionVariable());
|
||||
}
|
||||
for (Instruction instruction : block) {
|
||||
instruction.acceptVisitor(defExtractor);
|
||||
Variable[] varsDefinedByInstruction = defExtractor.getDefinedVariables();
|
||||
if (varsDefinedByInstruction != null) {
|
||||
varsDefinedInBlock.addAll(Arrays.asList(varsDefinedByInstruction));
|
||||
}
|
||||
}
|
||||
return varsDefinedInBlock;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,8 @@ public class RegisterAllocator {
|
|||
Program program = phi.getBasicBlock().getProgram();
|
||||
AssignInstruction copyInstruction = new AssignInstruction();
|
||||
Variable firstCopy = program.createVariable();
|
||||
firstCopy.setLabel(phi.getReceiver().getLabel());
|
||||
firstCopy.setDebugName(phi.getReceiver().getDebugName());
|
||||
copyInstruction.setReceiver(firstCopy);
|
||||
copyInstruction.setAssignee(incoming.getValue());
|
||||
BasicBlock source = blockMap.get(incoming.getSource());
|
||||
|
|
|
@ -476,7 +476,7 @@ public class OutputStreamWriterTest {
|
|||
assertEquals("invalid conversion 4", "\u001b$B$($(\u001b(B", new String(bout.toByteArray(), "ISO8859_1"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Can't test missing converter
|
||||
System.out.println(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user