mirror of
https://github.com/irmen/prog8.git
synced 2025-01-12 04:30:03 +00:00
taking down the heapvalue mess further
This commit is contained in:
parent
984d251a6d
commit
c717f4573d
@ -429,7 +429,7 @@ private fun prog8Parser.ExpressionContext.toAst() : Expression {
|
||||
else -> throw FatalAstException("invalid datatype for numeric literal")
|
||||
}
|
||||
litval.floatliteral()!=null -> NumericLiteralValue(DataType.FLOAT, litval.floatliteral().toAst(), litval.toPosition())
|
||||
litval.stringliteral()!=null -> StringLiteralValue(DataType.STR, unescape(litval.stringliteral().text, litval.toPosition()), position = litval.toPosition())
|
||||
litval.stringliteral()!=null -> StringLiteralValue(DataType.STR, unescape(litval.stringliteral().text, litval.toPosition()), null, litval.toPosition())
|
||||
litval.charliteral()!=null -> {
|
||||
try {
|
||||
NumericLiteralValue(DataType.UBYTE, Petscii.encodePetscii(unescape(litval.charliteral().text, litval.toPosition()), true)[0], litval.toPosition())
|
||||
|
@ -414,13 +414,10 @@ class StructLiteralValue(var values: List<Expression>,
|
||||
|
||||
class StringLiteralValue(val type: DataType, // only string types
|
||||
val value: String,
|
||||
initHeapId: Int? =null,
|
||||
var heapId: Int?,
|
||||
override val position: Position) : Expression() {
|
||||
override lateinit var parent: Node
|
||||
|
||||
var heapId = initHeapId
|
||||
private set
|
||||
|
||||
override fun linkParents(parent: Node) {
|
||||
this.parent = parent
|
||||
}
|
||||
@ -441,8 +438,10 @@ class StringLiteralValue(val type: DataType, // only string types
|
||||
fun addToHeap(heap: HeapValues) {
|
||||
if (heapId != null)
|
||||
return
|
||||
else
|
||||
heapId = heap.addString(type, value)
|
||||
else {
|
||||
val encodedStr = Petscii.encodePetscii(value, true)
|
||||
heapId = heap.addIntegerArray(DataType.ARRAY_UB, encodedStr.map { IntegerOrAddressOf(it.toInt(), null)}.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -965,8 +965,7 @@ internal class AstChecker(private val program: Program,
|
||||
} else if(target.datatype in StringDatatypes) {
|
||||
if(target.value is StringLiteralValue) {
|
||||
// check string lengths for non-memory mapped strings
|
||||
val heapId = (target.value as StringLiteralValue).heapId!!
|
||||
val stringLen = program.heap.get(heapId).str!!.length
|
||||
val stringLen = (target.value as StringLiteralValue).value.length
|
||||
val index = (arrayIndexedExpression.arrayspec.index as? NumericLiteralValue)?.number?.toInt()
|
||||
if (index != null && (index < 0 || index >= stringLen))
|
||||
checkResult.add(ExpressionError("index out of bounds", arrayIndexedExpression.arrayspec.position))
|
||||
@ -1231,7 +1230,7 @@ internal class AstChecker(private val program: Program,
|
||||
return correct
|
||||
}
|
||||
|
||||
val array = program.heap.get(value.heapId!!)
|
||||
val array = program.heap.get(value.heapId!!) // TODO use value.array directly?
|
||||
if(array.type !in ArrayDatatypes || (array.array==null && array.doubleArray==null))
|
||||
throw FatalAstException("should have an array in the heapvar $array")
|
||||
|
||||
|
@ -69,4 +69,3 @@ fun loadAsmIncludeFile(filename: String, source: Path): String {
|
||||
internal fun tryGetEmbeddedResource(name: String): InputStream? {
|
||||
return object{}.javaClass.getResourceAsStream("/prog8lib/$name")
|
||||
}
|
||||
|
||||
|
@ -2,19 +2,23 @@ package prog8.compiler
|
||||
|
||||
import prog8.ast.base.ArrayDatatypes
|
||||
import prog8.ast.base.DataType
|
||||
import prog8.ast.base.StringDatatypes
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The 'heapvalues' is the collection of variables that are allocated globally.
|
||||
* Arrays and strings belong here.
|
||||
* They get assigned a heapId to be able to retrieve them later.
|
||||
*/
|
||||
class HeapValues {
|
||||
data class HeapValue(val type: DataType, val str: String?, val array: Array<IntegerOrAddressOf>?, val doubleArray: DoubleArray?) {
|
||||
data class HeapValue(val type: DataType, val array: Array<IntegerOrAddressOf>?, val doubleArray: DoubleArray?) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as HeapValue
|
||||
return type==other.type && str==other.str && Arrays.equals(array, other.array) && Arrays.equals(doubleArray, other.doubleArray)
|
||||
return type==other.type && Arrays.equals(array, other.array) && Arrays.equals(doubleArray, other.doubleArray)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(str, array, doubleArray)
|
||||
override fun hashCode(): Int = Objects.hash(type, array, doubleArray)
|
||||
}
|
||||
|
||||
private val heap = mutableMapOf<Int, HeapValue>()
|
||||
@ -22,49 +26,28 @@ class HeapValues {
|
||||
|
||||
fun size(): Int = heap.size
|
||||
|
||||
fun addString(type: DataType, str: String): Int {
|
||||
if (str.length > 255)
|
||||
throw IllegalArgumentException("string length must be 0-255")
|
||||
|
||||
// strings are 'interned' and shared if they're the isSameAs
|
||||
val value = HeapValue(type, str, null, null)
|
||||
|
||||
val existing = heap.filter { it.value==value }.map { it.key }.firstOrNull()
|
||||
if(existing!=null)
|
||||
return existing
|
||||
val newId = heapId++
|
||||
heap[newId] = value
|
||||
return newId
|
||||
}
|
||||
|
||||
fun addIntegerArray(type: DataType, array: Array<IntegerOrAddressOf>): Int {
|
||||
// arrays are never shared, don't check for existing
|
||||
if(type !in ArrayDatatypes)
|
||||
throw CompilerException("wrong array type")
|
||||
throw CompilerException("wrong array type $type")
|
||||
val newId = heapId++
|
||||
heap[newId] = HeapValue(type, null, array, null)
|
||||
heap[newId] = HeapValue(type, array, null)
|
||||
return newId
|
||||
}
|
||||
|
||||
fun addDoublesArray(darray: DoubleArray): Int {
|
||||
// arrays are never shared, don't check for existing
|
||||
val newId = heapId++
|
||||
heap[newId] = HeapValue(DataType.ARRAY_F, null, null, darray)
|
||||
heap[newId] = HeapValue(DataType.ARRAY_F, null, darray)
|
||||
return newId
|
||||
}
|
||||
|
||||
fun updateString(heapId: Int, str: String) {
|
||||
val oldVal = heap[heapId] ?: throw IllegalArgumentException("heapId not found in heap")
|
||||
if(oldVal.type in StringDatatypes) {
|
||||
if (oldVal.str!!.length != str.length)
|
||||
throw IllegalArgumentException("heap string length mismatch")
|
||||
heap[heapId] = oldVal.copy(str = str)
|
||||
}
|
||||
else throw IllegalArgumentException("heap data type mismatch")
|
||||
}
|
||||
|
||||
fun get(heapId: Int): HeapValue {
|
||||
return heap[heapId] ?:
|
||||
throw IllegalArgumentException("heapId $heapId not found in heap")
|
||||
}
|
||||
|
||||
fun remove(heapId: Int) {
|
||||
heap.remove(heapId)
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ fun compileProgram(filepath: Path,
|
||||
programAst.checkValid(compilerOptions) // check if final tree is valid
|
||||
programAst.checkRecursion() // check if there are recursive subroutine calls
|
||||
|
||||
// printAst(programAst)
|
||||
printAst(programAst)
|
||||
|
||||
if(writeAssembly) {
|
||||
// asm generation directly from the Ast, no need for intermediate code
|
||||
|
@ -27,10 +27,15 @@ internal class StatementOptimizer(private val program: Program) : IAstModifyingV
|
||||
|
||||
private val pureBuiltinFunctions = BuiltinFunctions.filter { it.value.pure }
|
||||
private val callgraph = CallGraph(program)
|
||||
private val vardeclsToRemove = mutableListOf<VarDecl>()
|
||||
|
||||
override fun visit(program: Program) {
|
||||
removeUnusedCode(callgraph)
|
||||
super.visit(program)
|
||||
|
||||
for(decl in vardeclsToRemove) {
|
||||
decl.definingScope().remove(decl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeUnusedCode(callgraph: CallGraph) {
|
||||
@ -165,24 +170,33 @@ internal class StatementOptimizer(private val program: Program) : IAstModifyingV
|
||||
if(functionCallStatement.target.nameInSource==listOf("c64scr", "print") ||
|
||||
functionCallStatement.target.nameInSource==listOf("c64scr", "print_p")) {
|
||||
// printing a literal string of just 2 or 1 characters is replaced by directly outputting those characters
|
||||
val stringVar = functionCallStatement.arglist.single() as? IdentifierReference
|
||||
val arg = functionCallStatement.arglist.single()
|
||||
val stringVar: IdentifierReference?
|
||||
if(arg is AddressOf) {
|
||||
stringVar = arg.identifier
|
||||
} else {
|
||||
stringVar = arg as? IdentifierReference
|
||||
}
|
||||
if(stringVar!=null) {
|
||||
val heapId = stringVar.heapId(program.namespace)
|
||||
val string = program.heap.get(heapId).str!!
|
||||
if(string.length==1) {
|
||||
val petscii = Petscii.encodePetscii(string, true)[0]
|
||||
val vardecl = stringVar.targetVarDecl(program.namespace)!!
|
||||
val string = vardecl.value!! as StringLiteralValue
|
||||
val encodedString = Petscii.encodePetscii(string.value, true)
|
||||
if(string.value.length==1) {
|
||||
functionCallStatement.arglist.clear()
|
||||
functionCallStatement.arglist.add(NumericLiteralValue.optimalInteger(petscii.toInt(), functionCallStatement.position))
|
||||
functionCallStatement.arglist.add(NumericLiteralValue.optimalInteger(encodedString[0].toInt(), functionCallStatement.position))
|
||||
functionCallStatement.target = IdentifierReference(listOf("c64", "CHROUT"), functionCallStatement.target.position)
|
||||
vardeclsToRemove.add(vardecl)
|
||||
program.heap.remove(string.heapId!!)
|
||||
optimizationsDone++
|
||||
return functionCallStatement
|
||||
} else if(string.length==2) {
|
||||
val petscii = Petscii.encodePetscii(string, true)
|
||||
} else if(string.value.length==2) {
|
||||
val scope = AnonymousScope(mutableListOf(), functionCallStatement.position)
|
||||
scope.statements.add(FunctionCallStatement(IdentifierReference(listOf("c64", "CHROUT"), functionCallStatement.target.position),
|
||||
mutableListOf(NumericLiteralValue.optimalInteger(petscii[0].toInt(), functionCallStatement.position)), functionCallStatement.position))
|
||||
mutableListOf(NumericLiteralValue.optimalInteger(encodedString[0].toInt(), functionCallStatement.position)), functionCallStatement.position))
|
||||
scope.statements.add(FunctionCallStatement(IdentifierReference(listOf("c64", "CHROUT"), functionCallStatement.target.position),
|
||||
mutableListOf(NumericLiteralValue.optimalInteger(petscii[1].toInt(), functionCallStatement.position)), functionCallStatement.position))
|
||||
mutableListOf(NumericLiteralValue.optimalInteger(encodedString[1].toInt(), functionCallStatement.position)), functionCallStatement.position))
|
||||
vardeclsToRemove.add(vardecl)
|
||||
program.heap.remove(string.heapId!!)
|
||||
optimizationsDone++
|
||||
return scope
|
||||
}
|
||||
|
@ -662,8 +662,8 @@ class AstVm(val program: Program) {
|
||||
"c64scr.print" -> {
|
||||
// if the argument is an UWORD, consider it to be the "address" of the string (=heapId)
|
||||
if (args[0].wordval != null) {
|
||||
val str = program.heap.get(args[0].wordval!!).str!!
|
||||
dialog.canvas.printText(str, true)
|
||||
val encodedStr = program.heap.get(args[0].wordval!!).array!!.map { it.integer!!.toShort() }
|
||||
dialog.canvas.printText(encodedStr)
|
||||
} else
|
||||
throw VmExecutionException("print non-heap string")
|
||||
}
|
||||
@ -738,10 +738,11 @@ class AstVm(val program: Program) {
|
||||
}
|
||||
val inputStr = input.joinToString("")
|
||||
val heapId = args[0].wordval!!
|
||||
val origStr = program.heap.get(heapId).str!!
|
||||
val paddedStr=inputStr.padEnd(origStr.length+1, '\u0000').substring(0, origStr.length)
|
||||
program.heap.updateString(heapId, paddedStr)
|
||||
result = RuntimeValueNumeric(DataType.UBYTE, paddedStr.indexOf('\u0000'))
|
||||
val origStrLength = program.heap.get(heapId).array!!.size
|
||||
val encodedStr = Petscii.encodePetscii(inputStr, true).take(origStrLength).toMutableList()
|
||||
while(encodedStr.size<origStrLength)
|
||||
encodedStr.add(0)
|
||||
result = RuntimeValueNumeric(DataType.UBYTE, encodedStr.indexOf(0))
|
||||
}
|
||||
"c64flt.print_f" -> {
|
||||
dialog.canvas.printText(args[0].floatval.toString(), false)
|
||||
@ -761,8 +762,8 @@ class AstVm(val program: Program) {
|
||||
}
|
||||
"c64utils.str2uword" -> {
|
||||
val heapId = args[0].wordval!!
|
||||
val argString = program.heap.get(heapId).str!!
|
||||
val numericpart = argString.takeWhile { it.isDigit() }
|
||||
val argString = program.heap.get(heapId).array!!.map { it.integer!!.toChar() }
|
||||
val numericpart = argString.takeWhile { it.isDigit() }.toString()
|
||||
result = RuntimeValueNumeric(DataType.UWORD, numericpart.toInt() and 65535)
|
||||
}
|
||||
else -> TODO("syscall ${sub.scopedname} $sub")
|
||||
|
@ -75,6 +75,10 @@ class BitmapScreenPanel : KeyListener, JPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun printText(text: Iterable<Short>) {
|
||||
text.forEach { printPetscii(it, false) }
|
||||
}
|
||||
|
||||
fun printPetscii(char: Short, inverseVideo: Boolean=false) {
|
||||
if(char==13.toShort() || char==141.toShort()) {
|
||||
cursorX=0
|
||||
|
@ -83,8 +83,8 @@ class TestParserNumericLiteralValue {
|
||||
|
||||
@Test
|
||||
fun testEqualsRef() {
|
||||
assertTrue(StringLiteralValue(DataType.STR, "hello", position = dummyPos) == StringLiteralValue(DataType.STR, "hello", position = dummyPos))
|
||||
assertFalse(StringLiteralValue(DataType.STR, "hello", position = dummyPos) == StringLiteralValue(DataType.STR, "bye", position = dummyPos))
|
||||
assertTrue(StringLiteralValue(DataType.STR, "hello", null, dummyPos) == StringLiteralValue(DataType.STR, "hello", null, dummyPos))
|
||||
assertFalse(StringLiteralValue(DataType.STR, "hello", null, dummyPos) == StringLiteralValue(DataType.STR, "bye", null, dummyPos))
|
||||
|
||||
val lvOne = NumericLiteralValue(DataType.UBYTE, 1, dummyPos)
|
||||
val lvTwo = NumericLiteralValue(DataType.UBYTE, 2, dummyPos)
|
||||
@ -93,9 +93,9 @@ class TestParserNumericLiteralValue {
|
||||
val lvTwoR = NumericLiteralValue(DataType.UBYTE, 2, dummyPos)
|
||||
val lvThreeR = NumericLiteralValue(DataType.UBYTE, 3, dummyPos)
|
||||
val lvFour= NumericLiteralValue(DataType.UBYTE, 4, dummyPos)
|
||||
val lv1 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOne, lvTwo, lvThree), position = dummyPos)
|
||||
val lv2 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOneR, lvTwoR, lvThreeR), position = dummyPos)
|
||||
val lv3 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOneR, lvTwoR, lvFour), position = dummyPos)
|
||||
val lv1 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOne, lvTwo, lvThree), null, dummyPos)
|
||||
val lv2 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOneR, lvTwoR, lvThreeR), null, dummyPos)
|
||||
val lv3 = ArrayLiteralValue(DataType.ARRAY_UB, arrayOf(lvOneR, lvTwoR, lvFour), null, dummyPos)
|
||||
assertEquals(lv1, lv2)
|
||||
assertNotEquals(lv1, lv3)
|
||||
}
|
||||
|
@ -371,8 +371,8 @@ class TestPetscii {
|
||||
assertTrue(ten <= ten)
|
||||
assertFalse(ten < ten)
|
||||
|
||||
val abc = StringLiteralValue(DataType.STR, "abc", position = Position("", 0, 0, 0))
|
||||
val abd = StringLiteralValue(DataType.STR, "abd", position = Position("", 0, 0, 0))
|
||||
val abc = StringLiteralValue(DataType.STR, "abc", null, Position("", 0, 0, 0))
|
||||
val abd = StringLiteralValue(DataType.STR, "abd", null, Position("", 0, 0, 0))
|
||||
assertEquals(abc, abc)
|
||||
assertTrue(abc!=abd)
|
||||
assertFalse(abc!=abc)
|
||||
|
156
examples/test.p8
156
examples/test.p8
@ -12,80 +12,94 @@ main {
|
||||
str_s strs2 = "test"
|
||||
|
||||
sub start() {
|
||||
str str1x = "irmen"
|
||||
str str2x = "test"
|
||||
str_s strs1x = "irmen"
|
||||
str_s strs2x = "test"
|
||||
|
||||
c64scr.print(str1)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str2)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str1x)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str2x)
|
||||
c64.CHROUT('\n')
|
||||
str foo1 = "1\n"
|
||||
str foo2 = "12\n"
|
||||
|
||||
str1[0]='a'
|
||||
str2[0]='a'
|
||||
str1x[0]='a'
|
||||
str2x[0]='a'
|
||||
strs1x[0]='a'
|
||||
strs2x[0]='a'
|
||||
strs1[0]='a'
|
||||
strs2[0]='a'
|
||||
c64scr.print(foo1)
|
||||
c64scr.print(foo2)
|
||||
c64scr.print("\n")
|
||||
c64scr.print("1\n")
|
||||
c64scr.print("12\n")
|
||||
c64scr.print("\n")
|
||||
c64scr.print("1\n")
|
||||
c64scr.print("12\n")
|
||||
|
||||
; @TODO fix AstVm handling of strings (they're not modified right now) NOTE: array's seem to work fine
|
||||
c64scr.print(str1)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str2)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str1x)
|
||||
c64.CHROUT('\n')
|
||||
c64scr.print(str2x)
|
||||
c64.CHROUT('\n')
|
||||
|
||||
|
||||
byte[] barr = [1,2,3]
|
||||
word[] warr = [1000,2000,3000]
|
||||
float[] farr = [1.1, 2.2, 3.3]
|
||||
|
||||
byte bb
|
||||
word ww
|
||||
float ff
|
||||
for bb in barr {
|
||||
c64scr.print_b(bb)
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
c64.CHROUT('\n')
|
||||
for ww in warr {
|
||||
c64scr.print_w(ww)
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
c64.CHROUT('\n')
|
||||
for bb in 0 to len(farr)-1 {
|
||||
c64flt.print_f(farr[bb])
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
c64.CHROUT('\n')
|
||||
|
||||
barr[0] = 99
|
||||
warr[0] = 99
|
||||
farr[0] = 99.9
|
||||
for bb in barr {
|
||||
c64scr.print_b(bb)
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
c64.CHROUT('\n')
|
||||
for ww in warr {
|
||||
c64scr.print_w(ww)
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
c64.CHROUT('\n')
|
||||
for bb in 0 to len(farr)-1 {
|
||||
c64flt.print_f(farr[bb])
|
||||
c64.CHROUT(',')
|
||||
}
|
||||
; str str1x = "irmen"
|
||||
; str str2x = "test"
|
||||
; str_s strs1x = "irmen"
|
||||
; str_s strs2x = "test"
|
||||
;
|
||||
; c64scr.print("yoooooo")
|
||||
; c64scr.print(str1)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str2)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str1x)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str2x)
|
||||
; c64.CHROUT('\n')
|
||||
;
|
||||
; str1[0]='a'
|
||||
; str2[0]='a'
|
||||
; str1x[0]='a'
|
||||
; str2x[0]='a'
|
||||
; strs1x[0]='a'
|
||||
; strs2x[0]='a'
|
||||
; strs1[0]='a'
|
||||
; strs2[0]='a'
|
||||
;
|
||||
; ; @TODO fix AstVm handling of strings (they're not modified right now) NOTE: array's seem to work fine
|
||||
; c64scr.print(str1)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str2)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str1x)
|
||||
; c64.CHROUT('\n')
|
||||
; c64scr.print(str2x)
|
||||
; c64.CHROUT('\n')
|
||||
;
|
||||
;
|
||||
; byte[] barr = [1,2,3]
|
||||
; word[] warr = [1000,2000,3000]
|
||||
; float[] farr = [1.1, 2.2, 3.3]
|
||||
;
|
||||
; byte bb
|
||||
; word ww
|
||||
; float ff
|
||||
; for bb in barr {
|
||||
; c64scr.print_b(bb)
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
; c64.CHROUT('\n')
|
||||
; for ww in warr {
|
||||
; c64scr.print_w(ww)
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
; c64.CHROUT('\n')
|
||||
; for bb in 0 to len(farr)-1 {
|
||||
; c64flt.print_f(farr[bb])
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
; c64.CHROUT('\n')
|
||||
;
|
||||
; barr[0] = 99
|
||||
; warr[0] = 99
|
||||
; farr[0] = 99.9
|
||||
; for bb in barr {
|
||||
; c64scr.print_b(bb)
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
; c64.CHROUT('\n')
|
||||
; for ww in warr {
|
||||
; c64scr.print_w(ww)
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
; c64.CHROUT('\n')
|
||||
; for bb in 0 to len(farr)-1 {
|
||||
; c64flt.print_f(farr[bb])
|
||||
; c64.CHROUT(',')
|
||||
; }
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user