mirror of
https://github.com/byteworksinc/ORCA-C.git
synced 2024-12-30 14:31:04 +00:00
Fix type checking and result type computation for ? : operator.
This was non-standard in various ways, mainly in regard to pointer types. It has been rewritten to closely follow the specification in the C standards. Several helper functions dealing with types have been introduced. They are currently only used for ? :, but they might also be useful for other purposes. New tests are also introduced to check the behavior for the ? : operator. This fixes #35 (including the initializer-specific case).
This commit is contained in:
parent
15dc3a46c4
commit
102d6873a3
133
Expression.pas
133
Expression.pas
@ -65,6 +65,7 @@ var
|
||||
{----}
|
||||
lastwasconst: boolean; {did the last GenerateCode result in an integer constant?}
|
||||
lastconst: longint; {last integer constant from GenerateCode}
|
||||
lastWasNullPtrConst: boolean; {did last GenerateCode give a null ptr const?}
|
||||
{---------------------------------------------------------------}
|
||||
|
||||
procedure AssignmentConversion (t1, t2: typePtr; isConstant: boolean;
|
||||
@ -570,6 +571,25 @@ if tree <> nil then begin
|
||||
end; {DisposeTree}
|
||||
|
||||
|
||||
procedure ValueExpressionConversions;
|
||||
|
||||
{ Perform type conversions applicable to an expression used }
|
||||
{ for its value. These include lvalue conversion (removing }
|
||||
{ qualifiers), array-to-pointer conversion, and }
|
||||
{ function-to-pointer conversion. See C17 section 6.3.2.1. }
|
||||
{ }
|
||||
{ variables: }
|
||||
{ expressionType - set to type after conversions }
|
||||
|
||||
begin {ValueExpressionConversions}
|
||||
expressionType := Unqualify(expressionType);
|
||||
if expressionType^.kind = arrayType then
|
||||
expressionType := MakePointerTo(expressionType^.aType)
|
||||
else if expressionType^.kind = functionType then
|
||||
expressionType := MakePointerTo(expressionType);
|
||||
end; {ValueExpressionConversions}
|
||||
|
||||
|
||||
procedure AssignmentConversion {t1, t2: typePtr; isConstant: boolean;
|
||||
value: longint; genCode, checkConst: boolean};
|
||||
|
||||
@ -2772,7 +2792,7 @@ var
|
||||
doingScalar: boolean; {temp; for assignment operators}
|
||||
et: baseTypeEnum; {temp storage for a base type}
|
||||
i: integer; {loop variable}
|
||||
isString: boolean; {was the ? : a string?}
|
||||
isNullPtrConst: boolean; {is this a null pointer constant?}
|
||||
isVolatile: boolean; {is this a volatile op?}
|
||||
lType: typePtr; {type of operands}
|
||||
kind: typeKind; {temp type kind}
|
||||
@ -2780,6 +2800,7 @@ var
|
||||
t1: integer; {temporary work space label number}
|
||||
tlastwasconst: boolean; {temp lastwasconst}
|
||||
tlastconst: longint; {temp lastconst}
|
||||
tlastWasNullPtrConst: boolean; {temp lastWasNullPtrConst}
|
||||
tp: tokenPtr; {work pointer}
|
||||
tType: typePtr; {temp type of operand}
|
||||
|
||||
@ -3528,6 +3549,7 @@ var
|
||||
|
||||
begin {GenerateCode}
|
||||
lastwasconst := false;
|
||||
isNullPtrConst := false;
|
||||
case tree^.token.kind of
|
||||
|
||||
parameterOper:
|
||||
@ -3590,6 +3612,7 @@ case tree^.token.kind of
|
||||
Gen1t(pc_ldc, tree^.token.ival, cgWord);
|
||||
lastwasconst := true;
|
||||
lastconst := tree^.token.ival;
|
||||
isNullPtrConst := tree^.token.ival = 0;
|
||||
if tree^.token.kind = intConst then
|
||||
expressionType := intPtr
|
||||
else if tree^.token.kind = uintConst then
|
||||
@ -3612,6 +3635,7 @@ case tree^.token.kind of
|
||||
expressionType := ulongPtr;
|
||||
lastwasconst := true;
|
||||
lastconst := tree^.token.lval;
|
||||
isNullPtrConst := tree^.token.lval = 0;
|
||||
end; {case longConst}
|
||||
|
||||
longlongConst,ulonglongConst: begin
|
||||
@ -3624,6 +3648,7 @@ case tree^.token.kind of
|
||||
lastwasconst := true;
|
||||
lastconst := tree^.token.qval.lo;
|
||||
end; {if}
|
||||
isNullPtrConst := (tree^.token.qval.hi = 0) and (tree^.token.qval.lo = 0);
|
||||
end; {case longlongConst}
|
||||
|
||||
floatConst: begin
|
||||
@ -4565,78 +4590,71 @@ case tree^.token.kind of
|
||||
GenerateCode(tree^.left); {evaluate the condition}
|
||||
CompareToZero(pc_neq);
|
||||
GenerateCode(tree^.middle); {evaluate true expression}
|
||||
ValueExpressionConversions;
|
||||
lType := expressionType;
|
||||
tlastwasconst := lastwasconst;
|
||||
tlastconst := lastconst;
|
||||
tlastWasNullPtrConst := lastWasNullPtrConst;
|
||||
GenerateCode(tree^.right); {evaluate false expression}
|
||||
isString := false; {handle string operands}
|
||||
if lType^.kind in [arrayType,pointerType] then
|
||||
if lType^.aType^.baseType = cgUByte then begin
|
||||
with expressionType^ do
|
||||
if kind in [arrayType,pointerType] then begin
|
||||
if aType^.baseType = cgUByte then
|
||||
isString := true
|
||||
else if (kind = pointerType)
|
||||
and (CompTypes(lType,expressionType)) then
|
||||
{it's all OK}
|
||||
else
|
||||
Error(47)
|
||||
end {if}
|
||||
else if (kind = scalarType)
|
||||
and lastWasConst
|
||||
and (lastConst = 0) then
|
||||
et := UsualBinaryConversions(lType)
|
||||
{it's all OK}
|
||||
else
|
||||
ValueExpressionConversions;
|
||||
{check, compute, and convert types}
|
||||
if (lType^.kind = pointerType) or (expressionType^.kind = pointerType)
|
||||
then begin
|
||||
if tlastWasNullPtrConst then begin
|
||||
if lType^.kind = scalarType then
|
||||
Gen2(pc_cnn, ord(lType^.baseType), ord(cgULong));
|
||||
end {if}
|
||||
else if lastWasNullPtrConst then begin
|
||||
if expressionType^.kind = scalarType then
|
||||
Gen2(pc_cnv, ord(expressionType^.baseType), ord(cgULong));
|
||||
expressionType := lType;
|
||||
end {if}
|
||||
else if lType^.kind <> expressionType^.kind then {not both pointers}
|
||||
Error(47)
|
||||
else if IsVoid(lType^.pType) or IsVoid(expressionType^.pType) then begin
|
||||
if not looseTypeChecks then
|
||||
if (lType^.pType^.kind = functionType) or
|
||||
(expressionType^.pType^.kind = functionType) then
|
||||
Error(47);
|
||||
lType := voidPtrPtr;
|
||||
expressionType := voidPtrPtr;
|
||||
end; {if}
|
||||
with expressionType^ do
|
||||
if kind in [arrayType,pointerType] then
|
||||
if aType^.baseType in [cgByte,cgUByte] then begin
|
||||
if kind = pointerType then begin
|
||||
if tlastwasconst and (tlastconst = 0) then
|
||||
{it's all OK}
|
||||
else if CompTypes(lType, expressionType) then
|
||||
{it's all OK}
|
||||
else
|
||||
Error(47);
|
||||
end {if}
|
||||
else
|
||||
expressionType := MakePointerTo(MakeQualifiedType(voidPtr,
|
||||
lType^.pType^.qualifiers+expressionType^.pType^.qualifiers));
|
||||
end {else if}
|
||||
else if CompTypes(Unqualify(lType^.pType),
|
||||
Unqualify(expressionType^.pType)) then begin
|
||||
if not looseTypeChecks then
|
||||
if not StrictCompTypes(Unqualify(lType^.pType),
|
||||
Unqualify(expressionType^.pType)) then
|
||||
Error(47);
|
||||
et := UsualBinaryConversions(lType);
|
||||
lType := voidPtrPtr;
|
||||
expressionType := voidPtrPtr;
|
||||
end; {if}
|
||||
{generate the operation}
|
||||
if lType^.kind in [structType, unionType, arrayType] then begin
|
||||
expressionType := MakePointerTo(MakeQualifiedType(MakeCompositeType(
|
||||
Unqualify(lType^.pType),Unqualify(expressionType^.pType)),
|
||||
lType^.pType^.qualifiers+expressionType^.pType^.qualifiers));
|
||||
end {else if}
|
||||
else
|
||||
Error(47);
|
||||
et := cgULong;
|
||||
end {if}
|
||||
else if lType^.kind in [structType, unionType] then begin
|
||||
if not CompTypes(lType, expressionType) then
|
||||
Error(47);
|
||||
Gen0(pc_bno);
|
||||
Gen0t(pc_tri, cgULong);
|
||||
et := cgULong;
|
||||
end {if}
|
||||
else begin
|
||||
if expressionType^.kind = pointerType then
|
||||
tType := expressionType
|
||||
else
|
||||
tType := lType;
|
||||
if (expressionType^.kind = scalarType)
|
||||
and (expressionType^.baseType = cgVoid)
|
||||
and (lType^.kind = scalarType)
|
||||
and (lType^.baseType = cgVoid) then
|
||||
if IsVoid(lType) and IsVoid(expressionType) then
|
||||
et := cgVoid
|
||||
else
|
||||
et := UsualBinaryConversions(lType);
|
||||
Gen0(pc_bno);
|
||||
Gen0t(pc_tri, et);
|
||||
end; {else}
|
||||
if isString then {set the type for strings}
|
||||
expressionType := stringTypePtr;
|
||||
{generate the operation}
|
||||
Gen0(pc_bno);
|
||||
Gen0t(pc_tri, et);
|
||||
end; {case colonch}
|
||||
|
||||
castoper: begin {(cast)}
|
||||
GenerateCode(tree^.left);
|
||||
if lastWasNullPtrConst then
|
||||
if expressionType^.kind = scalarType then
|
||||
if tree^.castType^.kind = pointerType then
|
||||
if IsVoid(tree^.castType^.pType) then
|
||||
if tree^.castType^.pType^.qualifiers = [] then
|
||||
isNullPtrConst := true;
|
||||
Cast(tree^.castType);
|
||||
end; {case castoper}
|
||||
|
||||
@ -4646,6 +4664,7 @@ case tree^.token.kind of
|
||||
end; {case}
|
||||
if doDispose then
|
||||
dispose(tree);
|
||||
lastWasNullPtrConst := isNullPtrConst;
|
||||
end; {GenerateCode}
|
||||
|
||||
|
||||
|
157
Symbol.pas
157
Symbol.pas
@ -164,6 +164,16 @@ procedure InitSymbol;
|
||||
{ Initialize the symbol table module }
|
||||
|
||||
|
||||
function IsVoid (tp: typePtr): boolean;
|
||||
|
||||
{ Check to see if a type is void }
|
||||
{ }
|
||||
{ Parameters: }
|
||||
{ tp - type to check }
|
||||
{ }
|
||||
{ Returns: True if the type is void, else false }
|
||||
|
||||
|
||||
function LabelToDisp (lab: integer): integer; extern;
|
||||
|
||||
{ convert a local label number to a stack frame displacement }
|
||||
@ -192,6 +202,17 @@ function MakePointerTo (pType: typePtr): typePtr;
|
||||
{ returns: the pointer type }
|
||||
|
||||
|
||||
function MakeCompositeType (t1, t2: typePtr): typePtr;
|
||||
|
||||
{ Make the composite type of two compatible types. }
|
||||
{ See C17 section 6.2.7. }
|
||||
{ }
|
||||
{ parameters: }
|
||||
{ t1,t2 - the input types (must be compatible) }
|
||||
{ }
|
||||
{ returns: pointer to the composite type }
|
||||
|
||||
|
||||
function MakeQualifiedType (origType: typePtr; qualifiers: typeQualifierSet):
|
||||
typePtr;
|
||||
|
||||
@ -427,26 +448,6 @@ var
|
||||
p1, p2: parameterPtr; {for tracing parameter lists}
|
||||
pt1,pt2: typePtr; {pointer types}
|
||||
|
||||
|
||||
function IsVoid (tp: typePtr): boolean;
|
||||
|
||||
{ Check to see if a type is void }
|
||||
{ }
|
||||
{ Parameters: }
|
||||
{ tp - type to check }
|
||||
{ }
|
||||
{ Returns: True if the type is void, else false }
|
||||
|
||||
begin {IsVoid}
|
||||
IsVoid := false;
|
||||
if tp = voidPtr then
|
||||
IsVoid := true
|
||||
else if tp^.kind = scalarType then
|
||||
if tp^.baseType = cgVoid then
|
||||
IsVoid := true;
|
||||
end; {IsVoid}
|
||||
|
||||
|
||||
begin {CompTypes}
|
||||
CompTypes := false; {assume the types are not compatible}
|
||||
kind1 := t1^.kind; {get these for efficiency}
|
||||
@ -1703,6 +1704,122 @@ constCharPtr^.qualifiers := [tqConst];
|
||||
end; {InitSymbol}
|
||||
|
||||
|
||||
function IsVoid {tp: typePtr): boolean};
|
||||
|
||||
{ Check to see if a type is void }
|
||||
{ }
|
||||
{ Parameters: }
|
||||
{ tp - type to check }
|
||||
{ }
|
||||
{ Returns: True if the type is void, else false }
|
||||
|
||||
begin {IsVoid}
|
||||
IsVoid := false;
|
||||
if tp = voidPtr then
|
||||
IsVoid := true
|
||||
else if tp^.kind = scalarType then
|
||||
if tp^.baseType = cgVoid then
|
||||
IsVoid := true;
|
||||
end; {IsVoid}
|
||||
|
||||
|
||||
function CopyType (tp: typePtr): typePtr;
|
||||
|
||||
{ Make a new copy of a type, so it can be modified. }
|
||||
{ }
|
||||
{ Parameters: }
|
||||
{ tp - type to copy }
|
||||
{ }
|
||||
{ Returns: The new copy of the type }
|
||||
|
||||
var
|
||||
tType: typePtr; {the new copy of the type}
|
||||
p1,p2: parameterPtr; {parameter ptrs for copying prototypes}
|
||||
pPtr: ^parameterPtr; {temp for copying prototypes}
|
||||
|
||||
begin {CopyType}
|
||||
if tp^.kind in [structType,unionType] then
|
||||
Error(57);
|
||||
tType := pointer(Malloc(sizeof(typeRecord)));
|
||||
tType^ := tp^; {copy type record}
|
||||
tType^.saveDisp := 0;
|
||||
if tp^.kind = functionType then {copy prototype parameter list}
|
||||
if tp^.prototyped then begin
|
||||
p1 := tp^.parameterList;
|
||||
pPtr := @tType^.parameterList;
|
||||
while p1 <> nil do begin
|
||||
p2 := pointer(Malloc(sizeof(parameterRecord)));
|
||||
p2^ := p1^;
|
||||
pPtr^ := p2;
|
||||
pPtr := @p2^.next;
|
||||
p1 := p1^.next;
|
||||
end; {while}
|
||||
end; {if}
|
||||
CopyType := tType;
|
||||
end; {CopyType}
|
||||
|
||||
|
||||
function MakeCompositeType {t1, t2: typePtr): typePtr};
|
||||
|
||||
{ Make the composite type of two compatible types. }
|
||||
{ See C17 section 6.2.7. }
|
||||
{ }
|
||||
{ parameters: }
|
||||
{ t1,t2 - the input types (should be compatible) }
|
||||
{ }
|
||||
{ returns: pointer to the composite type }
|
||||
|
||||
var
|
||||
compType: typePtr; {the composite type}
|
||||
tType: typePtr; {temp type}
|
||||
p1,p2: parameterPtr; {parameter ptrs for handling prototypes}
|
||||
|
||||
begin {MakeCompositeType}
|
||||
compType := t2; {default to t2}
|
||||
if t1 <> t2 then
|
||||
if t1^.kind = t2^.kind then begin
|
||||
if t2^.kind = functionType then {switch fn types if only t1 is prototyped}
|
||||
if not t2^.prototyped then
|
||||
if t1^.prototyped then begin
|
||||
compType := t1;
|
||||
t1 := t2;
|
||||
t2 := compType;
|
||||
end; {if}
|
||||
{apply recursively for derived types}
|
||||
if t2^.kind in [arrayType,pointerType,functionType] then begin
|
||||
tType := MakeCompositeType(t1^.aType,t2^.aType);
|
||||
if tType <> t2^.aType then begin
|
||||
compType := CopyType(compType);
|
||||
compType^.aType := tType;
|
||||
end; {if}
|
||||
end; {if}
|
||||
if t2^.kind = arrayType then {get array size from t1 if needed}
|
||||
if t2^.size = 0 then
|
||||
if t1^.size <> 0 then
|
||||
if t1^.aType^.size = t2^.aType^.size then begin
|
||||
if compType = t2 then
|
||||
compType := CopyType(t2);
|
||||
CompType^.size := t1^.size;
|
||||
CompType^.elements := t1^.elements;
|
||||
end; {if}
|
||||
if t2^.kind = functionType then {compose function parameter types}
|
||||
if t1^.prototyped and t2^.prototyped then begin
|
||||
if compType = t2 then
|
||||
compType := CopyType(t2);
|
||||
p1 := t1^.parameterList;
|
||||
p2 := compType^.parameterList;
|
||||
while (p1 <> nil) and (p2 <> nil) do begin
|
||||
p2^.parameterType :=
|
||||
MakeCompositeType(p1^.parameterType,p2^.parameterType);
|
||||
p1 := p1^.next;
|
||||
p2 := p2^.next;
|
||||
end; {while}
|
||||
end;
|
||||
end; {if}
|
||||
MakeCompositeType := compType;
|
||||
end; {MakeCompositeType}
|
||||
|
||||
|
||||
function MakePascalType {origType: typePtr): typePtr};
|
||||
|
||||
{ make a version of a type with the pascal qualifier applied }
|
||||
|
@ -28,3 +28,4 @@
|
||||
{1} c11sassert.c
|
||||
{1} c11unicode.c
|
||||
{1} c11uchar.c
|
||||
{1} c11ternary.c
|
||||
|
57
Tests/Conformance/c11ternary.c
Normal file
57
Tests/Conformance/c11ternary.c
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Test the ? : operator.
|
||||
*
|
||||
* The basic properties tested should hold back to C89,
|
||||
* but a C11 feature (_Generic) is used to test them.
|
||||
*/
|
||||
|
||||
#define assert_type(e,t) (void)_Generic((e), t:(e))
|
||||
|
||||
int main(void) {
|
||||
int i = 1;
|
||||
long l = 2;
|
||||
double d = 3;
|
||||
struct S {int i;} s = {4};
|
||||
const struct S t = {5};
|
||||
const void *cvp = &i;
|
||||
void *vp = &i;
|
||||
const int *cip = &i;
|
||||
volatile int *vip = 0;
|
||||
int *ip = &i;
|
||||
const char *ccp = 0;
|
||||
int (*fp1)() = 0;
|
||||
int (*fp2)(int (*)[40]) = 0;
|
||||
int (*fp3)(int (*)[]) = 0;
|
||||
|
||||
assert_type(1?i:l, long);
|
||||
assert_type(1?d:i, double);
|
||||
assert_type(1?s:t, struct S);
|
||||
1?(void)2:(void)3;
|
||||
assert_type(1?ip:ip, int *);
|
||||
assert_type(1?ip:cip, const int *);
|
||||
assert_type(1?cip:ip, const int *);
|
||||
assert_type(1?0:ip, int *);
|
||||
assert_type(0?0LL:ip, int *);
|
||||
assert_type(1?(void*)0:ip, int *);
|
||||
assert_type(1?cip:0, const int *);
|
||||
assert_type(1?cip:0LL, const int *);
|
||||
assert_type(1?cip:(char)0.0, const int *);
|
||||
assert_type(1?cip:(void*)0, const int *);
|
||||
assert_type(1?(void*)(void*)0:ip, void *);
|
||||
assert_type(1?(void*)ip:ip, void *);
|
||||
assert_type(1?cip:(void*)(void*)0, const void *);
|
||||
assert_type(1?(void*)ip:cip, const void *);
|
||||
assert_type(1?main:main, int(*)(void));
|
||||
assert_type(1?main:0, int(*)(void));
|
||||
assert_type(1?(const void*)cip:(void*)ip, const void *);
|
||||
assert_type(1?cvp:cip, const void *);
|
||||
assert_type(1?vip:0, volatile int *);
|
||||
assert_type(1?cip:vip, const volatile int *);
|
||||
assert_type(1?vp:ccp, const void *);
|
||||
assert_type(1?ip:cip, const int *);
|
||||
assert_type(1?vp:ip, void *);
|
||||
assert_type(1?fp1:fp2, int (*)(int (*)[40]));
|
||||
assert_type(1?fp2:fp3, int (*)(int (*)[40]));
|
||||
assert_type(1?fp2:0, int (*)(int (*)[40]));
|
||||
assert_type(1?fp2:(void*)0, int (*)(int (*)[40]));
|
||||
}
|
27
Tests/Deviance/D7.8.0.1.CC
Normal file
27
Tests/Deviance/D7.8.0.1.CC
Normal file
@ -0,0 +1,27 @@
|
||||
/* Deviance Test 7.8.0.1: Ensure invalid operand types for ?: are detected */
|
||||
|
||||
int printf(const char *, ...);
|
||||
|
||||
int main(void) {
|
||||
int i = 1;
|
||||
struct S {int i;} s = {4};
|
||||
int *ip = &i;
|
||||
long *lp = 0;
|
||||
const int *cip = &i;
|
||||
|
||||
/* each statement below should give an error */
|
||||
1 ? i : s;
|
||||
1 ? s : i;
|
||||
1 ? i : (void)0;
|
||||
1 ? ip : lp;
|
||||
|
||||
/* these are illegal in standard C, but allowed by loose type checks */
|
||||
#pragma ignore 24
|
||||
1 ? main : (void*)(void*)0;
|
||||
1 ? &ip : &cip;
|
||||
|
||||
/* should give an error, but currently does not in ORCA/C */
|
||||
1 ? cip : (char)+0.0;
|
||||
|
||||
printf ("Failed Deviance Test 7.8.0.1\n");
|
||||
}
|
@ -51,6 +51,7 @@
|
||||
{1} D7.6.6.1.CC
|
||||
{1} D7.6.7.1.CC
|
||||
{1} D7.6.8.1.CC
|
||||
{1} D7.8.0.1.CC
|
||||
{1} D8.7.0.1.CC
|
||||
{1} D8.8.0.1.CC
|
||||
{1} D9.2.0.1.CC
|
||||
|
6
cc.notes
6
cc.notes
@ -584,7 +584,7 @@ First, setting bit 5 causes pointer assignments that discard type qualifiers to
|
||||
|
||||
Second, setting bit 5 causes type compatibility checks involving function pointers to ignore the prototyped parameter types. If bit 5 is clear, the prototyped parameter types (if available) must be compatible.
|
||||
|
||||
Third, setting bit 5 causes certain comparisons involving pointers to be permitted even though they violate constraints specified in the C standards. If bit 5 is clear, the rules in the standards will be followed strictly.
|
||||
Third, setting bit 5 causes certain comparisons involving pointers, as well as certain uses of the ? : operator with pointer operands, to be permitted even though they violate constraints specified in the C standards. If bit 5 is clear, the rules in the standards will be followed strictly.
|
||||
|
||||
Fourth, setting bit 5 causes ORCA/C to treat basic types with the same representation as mutually compatible. This affects the following pairs of types: short and int, unsigned short and unsigned int, char and unsigned char. Historically, ORCA/C essentially treated each of these pairs as being the same type, so it never reported type conflicts between them. If bit 5 is set, it will continue to do so. If bit 5 is clear, it will treat all of the above types as distinct and mutually incompatible, as specified by the C standards.
|
||||
|
||||
@ -1825,6 +1825,10 @@ int foo(int[42]);
|
||||
|
||||
(Devin Reade)
|
||||
|
||||
189. Some combinations of operand types were not properly supported for the ? : operator. This could result in a spurious error, or could cause the ? : expression to have an incorrect type. One case where a spurious error would be produced is for expressions like i?0:p, where p is a pointer.
|
||||
|
||||
(Jay Krell, Devin Reade)
|
||||
|
||||
-- Bugs from C 2.1.0 that have been fixed -----------------------------------
|
||||
|
||||
1. In some situations, fread() reread the first 1K or so of the file.
|
||||
|
Loading…
Reference in New Issue
Block a user