Commit a1ccbae6 by Iain Buclaw

d: Merge update dmd 799066f49

Removes the implementation of __traits(argTypes), which only supported
x86_64 targets.  The only use of this trait is an unused va_arg()
function, this has been removed as well.

Reviewed-on: https://github.com/dlang/dmd/pull/11022

gcc/d/ChangeLog:

2020-04-13  Iain Buclaw  <ibuclaw@gdcproject.org>

	* Make-lang.in (D_FRONTEND_OBJS): Remove d/argtypes.o.
	* d-target.cc (Target::toArgTypes): New function.

libphobos/ChangeLog:

2020-04-13  Iain Buclaw  <ibuclaw@gdcproject.org>

	* libdruntime/core/stdc/stdarg.d: Remove run-time va_list template.
parent af4c9257
2020-04-13 Iain Buclaw <ibuclaw@gdcproject.org>
* Make-lang.in (D_FRONTEND_OBJS): Remove d/argtypes.o.
* d-target.cc (Target::toArgTypes): New function.
2020-04-10 Iain Buclaw <ibuclaw@gdcproject.org> 2020-04-10 Iain Buclaw <ibuclaw@gdcproject.org>
* d-spec.cc (LIBDRUNTIME): Remove. * d-spec.cc (LIBDRUNTIME): Remove.
......
...@@ -59,7 +59,6 @@ D_FRONTEND_OBJS = \ ...@@ -59,7 +59,6 @@ D_FRONTEND_OBJS = \
d/access.o \ d/access.o \
d/aliasthis.o \ d/aliasthis.o \
d/apply.o \ d/apply.o \
d/argtypes.o \
d/arrayop.o \ d/arrayop.o \
d/attrib.o \ d/attrib.o \
d/blockexit.o \ d/blockexit.o \
......
...@@ -410,3 +410,15 @@ Target::systemLinkage (void) ...@@ -410,3 +410,15 @@ Target::systemLinkage (void)
{ {
return LINKc; return LINKc;
} }
/* Generate a TypeTuple of the equivalent types used to determine if a
function argument of the given type can be passed in registers.
The results of this are highly platform dependent, and intended
primarly for use in implementing va_arg() with RTTI. */
TypeTuple *
Target::toArgTypes (Type *)
{
/* Not implemented, however this is not currently used anywhere. */
return NULL;
}
3e10e2dd29e583f1d94d84de5e4bd858e0303669 799066f498aebcfa420df284cac1f204b1f953a8
The first line of this file holds the git revision number of the last The first line of this file holds the git revision number of the last
merge done from the dlang/dmd repository. merge done from the dlang/dmd repository.
/* Compiler implementation of the D programming language
* Copyright (C) 2010-2019 by The D Language Foundation, All Rights Reserved
* written by Walter Bright
* http://www.digitalmars.com
* Distributed under the Boost Software License, Version 1.0.
* http://www.boost.org/LICENSE_1_0.txt
* https://github.com/D-Programming-Language/dmd/blob/master/src/argtypes.c
*/
#include "root/dsystem.h"
#include "root/checkedint.h"
#include "mars.h"
#include "dsymbol.h"
#include "mtype.h"
#include "scope.h"
#include "init.h"
#include "expression.h"
#include "attrib.h"
#include "declaration.h"
#include "template.h"
#include "id.h"
#include "enum.h"
#include "import.h"
#include "aggregate.h"
#include "hdrgen.h"
/****************************************************
* This breaks a type down into 'simpler' types that can be passed to a function
* in registers, and returned in registers.
* It's highly platform dependent.
* Params:
* t = type to break down
* Returns:
* tuple of types, each element can be passed in a register.
* A tuple of zero length means the type cannot be passed/returned in registers.
*/
TypeTuple *toArgTypes(Type *t)
{
class ToArgTypes : public Visitor
{
public:
TypeTuple *result;
ToArgTypes()
{
result = NULL;
}
void visit(Type *)
{
// not valid for a parameter
}
void visit(TypeError *)
{
result = new TypeTuple(Type::terror);
}
void visit(TypeBasic *t)
{
Type *t1 = NULL;
Type *t2 = NULL;
switch (t->ty)
{
case Tvoid:
return;
case Tbool:
case Tint8:
case Tuns8:
case Tint16:
case Tuns16:
case Tint32:
case Tuns32:
case Tfloat32:
case Tint64:
case Tuns64:
case Tint128:
case Tuns128:
case Tfloat64:
case Tfloat80:
t1 = t;
break;
case Timaginary32:
t1 = Type::tfloat32;
break;
case Timaginary64:
t1 = Type::tfloat64;
break;
case Timaginary80:
t1 = Type::tfloat80;
break;
case Tcomplex32:
if (global.params.is64bit)
t1 = Type::tfloat64;
else
{
t1 = Type::tfloat64;
t2 = Type::tfloat64;
}
break;
case Tcomplex64:
t1 = Type::tfloat64;
t2 = Type::tfloat64;
break;
case Tcomplex80:
t1 = Type::tfloat80;
t2 = Type::tfloat80;
break;
case Tchar:
t1 = Type::tuns8;
break;
case Twchar:
t1 = Type::tuns16;
break;
case Tdchar:
t1 = Type::tuns32;
break;
default:
assert(0);
}
if (t1)
{
if (t2)
result = new TypeTuple(t1, t2);
else
result = new TypeTuple(t1);
}
else
result = new TypeTuple();
}
void visit(TypeVector *t)
{
result = new TypeTuple(t);
}
void visit(TypeSArray *t)
{
if (t->dim)
{
/* Should really be done as if it were a struct with dim members
* of the array's elements.
* I.e. int[2] should be done like struct S { int a; int b; }
*/
dinteger_t sz = t->dim->toInteger();
// T[1] should be passed like T
if (sz == 1)
{
t->next->accept(this);
return;
}
}
result = new TypeTuple(); // pass on the stack for efficiency
}
void visit(TypeAArray *)
{
result = new TypeTuple(Type::tvoidptr);
}
void visit(TypePointer *)
{
result = new TypeTuple(Type::tvoidptr);
}
/*************************************
* Convert a floating point type into the equivalent integral type.
*/
static Type *mergeFloatToInt(Type *t)
{
switch (t->ty)
{
case Tfloat32:
case Timaginary32:
t = Type::tint32;
break;
case Tfloat64:
case Timaginary64:
case Tcomplex32:
t = Type::tint64;
break;
default:
assert(0);
}
return t;
}
/*************************************
* This merges two types into an 8byte type.
* Params:
* t1 = first type (can be null)
* t2 = second type (can be null)
* offset2 = offset of t2 from start of t1
* Returns:
* type that encompasses both t1 and t2, null if cannot be done
*/
static Type *argtypemerge(Type *t1, Type *t2, unsigned offset2)
{
//printf("argtypemerge(%s, %s, %d)\n", t1 ? t1->toChars() : "", t2 ? t2->toChars() : "", offset2);
if (!t1)
{ assert(!t2 || offset2 == 0);
return t2;
}
if (!t2)
return t1;
const d_uns64 sz1 = t1->size(Loc());
const d_uns64 sz2 = t2->size(Loc());
assert(sz1 != SIZE_INVALID && sz2 != SIZE_INVALID);
if (t1->ty != t2->ty &&
(t1->ty == Tfloat80 || t2->ty == Tfloat80))
return NULL;
// [float,float] => [cfloat]
if (t1->ty == Tfloat32 && t2->ty == Tfloat32 && offset2 == 4)
return Type::tfloat64;
// Merging floating and non-floating types produces the non-floating type
if (t1->isfloating())
{
if (!t2->isfloating())
t1 = mergeFloatToInt(t1);
}
else if (t2->isfloating())
t2 = mergeFloatToInt(t2);
Type *t;
// Pick type with larger size
if (sz1 < sz2)
t = t2;
else
t = t1;
// If t2 does not lie within t1, need to increase the size of t to enclose both
assert(sz2 < UINT64_MAX - UINT32_MAX);
if (offset2 && sz1 < offset2 + sz2)
{
switch (offset2 + sz2)
{
case 2:
t = Type::tint16;
break;
case 3:
case 4:
t = Type::tint32;
break;
default:
t = Type::tint64;
break;
}
}
return t;
}
void visit(TypeDArray *)
{
/* Should be done as if it were:
* struct S { size_t length; void* ptr; }
*/
if (global.params.is64bit && !global.params.isLP64)
{
// For AMD64 ILP32 ABI, D arrays fit into a single integer register.
unsigned offset = (unsigned)Type::tsize_t->size(Loc());
Type *t = argtypemerge(Type::tsize_t, Type::tvoidptr, offset);
if (t)
{
result = new TypeTuple(t);
return;
}
}
result = new TypeTuple(Type::tsize_t, Type::tvoidptr);
}
void visit(TypeDelegate *)
{
/* Should be done as if it were:
* struct S { size_t length; void* ptr; }
*/
if (global.params.is64bit && !global.params.isLP64)
{
// For AMD64 ILP32 ABI, delegates fit into a single integer register.
unsigned offset = (unsigned)Type::tsize_t->size(Loc());
Type *t = argtypemerge(Type::tsize_t, Type::tvoidptr, offset);
if (t)
{
result = new TypeTuple(t);
return;
}
}
result = new TypeTuple(Type::tvoidptr, Type::tvoidptr);
}
void visit(TypeStruct *t)
{
//printf("TypeStruct::toArgTypes() %s\n", t->toChars());
if (!t->sym->isPOD() || t->sym->fields.dim == 0)
{
Lmemory:
//printf("\ttoArgTypes() %s => [ ]\n", t->toChars());
result = new TypeTuple(); // pass on the stack
return;
}
Type *t1 = NULL;
Type *t2 = NULL;
const d_uns64 sz = t->size(Loc());
assert(sz < 0xFFFFFFFF);
switch ((unsigned)sz)
{
case 1:
t1 = Type::tint8;
break;
case 2:
t1 = Type::tint16;
break;
case 3:
if (!global.params.is64bit)
goto Lmemory;
/* fall through */
case 4:
t1 = Type::tint32;
break;
case 5:
case 6:
case 7:
if (!global.params.is64bit)
goto Lmemory;
/* fall through */
case 8:
t1 = Type::tint64;
break;
case 16:
t1 = NULL; // could be a TypeVector
break;
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
if (!global.params.is64bit)
goto Lmemory;
t1 = NULL;
break;
default:
goto Lmemory;
}
if (global.params.is64bit && t->sym->fields.dim)
{
t1 = NULL;
for (size_t i = 0; i < t->sym->fields.dim; i++)
{
VarDeclaration *f = t->sym->fields[i];
//printf(" [%d] %s f->type = %s\n", (int)i, f->toChars(), f->type->toChars());
TypeTuple *tup = toArgTypes(f->type);
if (!tup)
goto Lmemory;
size_t dim = tup->arguments->dim;
Type *ft1 = NULL;
Type *ft2 = NULL;
switch (dim)
{
case 2:
ft1 = (*tup->arguments)[0]->type;
ft2 = (*tup->arguments)[1]->type;
break;
case 1:
if (f->offset < 8)
ft1 = (*tup->arguments)[0]->type;
else
ft2 = (*tup->arguments)[0]->type;
break;
default:
goto Lmemory;
}
if (f->offset & 7)
{
// Misaligned fields goto Lmemory
unsigned alignsz = f->type->alignsize();
if (f->offset & (alignsz - 1))
goto Lmemory;
// Fields that overlap the 8byte boundary goto Lmemory
const d_uns64 fieldsz = f->type->size(Loc());
assert(fieldsz != SIZE_INVALID && fieldsz < UINT64_MAX - UINT32_MAX);
if (f->offset < 8 && (f->offset + fieldsz) > 8)
goto Lmemory;
}
// First field in 8byte must be at start of 8byte
assert(t1 || f->offset == 0);
//printf("ft1 = %s\n", ft1 ? ft1->toChars() : "null");
//printf("ft2 = %s\n", ft2 ? ft2->toChars() : "null");
if (ft1)
{
t1 = argtypemerge(t1, ft1, f->offset);
if (!t1)
goto Lmemory;
}
if (ft2)
{
unsigned off2 = f->offset;
if (ft1)
off2 = 8;
if (!t2 && off2 != 8)
goto Lmemory;
assert(t2 || off2 == 8);
t2 = argtypemerge(t2, ft2, off2 - 8);
if (!t2)
goto Lmemory;
}
}
if (t2)
{
if (t1->isfloating() && t2->isfloating())
{
if ((t1->ty == Tfloat32 || t1->ty == Tfloat64) &&
(t2->ty == Tfloat32 || t2->ty == Tfloat64))
;
else
goto Lmemory;
}
else if (t1->isfloating())
goto Lmemory;
else if (t2->isfloating())
goto Lmemory;
else
{
}
}
}
//printf("\ttoArgTypes() %s => [%s,%s]\n", t->toChars(), t1 ? t1->toChars() : "", t2 ? t2->toChars() : "");
if (t1)
{
//if (t1) printf("test1: %s => %s\n", toChars(), t1->toChars());
if (t2)
result = new TypeTuple(t1, t2);
else
result = new TypeTuple(t1);
}
else
goto Lmemory;
}
void visit(TypeEnum *t)
{
t->toBasetype()->accept(this);
}
void visit(TypeClass *)
{
result = new TypeTuple(Type::tvoidptr);
}
};
ToArgTypes v;
t->accept(&v);
return v.result;
}
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
#include "statement.h" #include "statement.h"
#include "template.h" #include "template.h"
#include "tokens.h" #include "tokens.h"
#include "target.h"
Type *getTypeInfoType(Loc loc, Type *t, Scope *sc); Type *getTypeInfoType(Loc loc, Type *t, Scope *sc);
TypeTuple *toArgTypes(Type *t);
void unSpeculative(Scope *sc, RootObject *o); void unSpeculative(Scope *sc, RootObject *o);
bool MODimplicitConv(MOD modfrom, MOD modto); bool MODimplicitConv(MOD modfrom, MOD modto);
Expression *resolve(Loc loc, Scope *sc, Dsymbol *s, bool hasOverloads); Expression *resolve(Loc loc, Scope *sc, Dsymbol *s, bool hasOverloads);
...@@ -1303,8 +1303,8 @@ void StructDeclaration::finalizeSize() ...@@ -1303,8 +1303,8 @@ void StructDeclaration::finalizeSize()
} }
} }
TypeTuple *tt = toArgTypes(type); TypeTuple *tt = Target::toArgTypes(type);
size_t dim = tt->arguments->dim; size_t dim = tt ? tt->arguments->dim : 0;
if (dim >= 1) if (dim >= 1)
{ {
assert(dim <= 2); assert(dim <= 2);
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
bool typeMerge(Scope *sc, TOK op, Type **pt, Expression **pe1, Expression **pe2); bool typeMerge(Scope *sc, TOK op, Type **pt, Expression **pe1, Expression **pe2);
bool isArrayOpValid(Expression *e); bool isArrayOpValid(Expression *e);
Expression *expandVar(int result, VarDeclaration *v); Expression *expandVar(int result, VarDeclaration *v);
TypeTuple *toArgTypes(Type *t);
bool checkAssignEscape(Scope *sc, Expression *e, bool gag); bool checkAssignEscape(Scope *sc, Expression *e, bool gag);
bool checkParamArgumentEscape(Scope *sc, FuncDeclaration *fdc, Identifier *par, Expression *arg, bool gag); bool checkParamArgumentEscape(Scope *sc, FuncDeclaration *fdc, Identifier *par, Expression *arg, bool gag);
bool checkAccess(AggregateDeclaration *ad, Loc loc, Scope *sc, Dsymbol *smember); bool checkAccess(AggregateDeclaration *ad, Loc loc, Scope *sc, Dsymbol *smember);
...@@ -2074,7 +2073,7 @@ public: ...@@ -2074,7 +2073,7 @@ public:
* The results of this are highly platform dependent, and intended * The results of this are highly platform dependent, and intended
* primarly for use in implementing va_arg(). * primarly for use in implementing va_arg().
*/ */
tded = toArgTypes(e->targ); tded = Target::toArgTypes(e->targ);
if (!tded) if (!tded)
goto Lno; // not valid for a parameter goto Lno; // not valid for a parameter
break; break;
......
...@@ -21,6 +21,7 @@ class Dsymbol; ...@@ -21,6 +21,7 @@ class Dsymbol;
class Expression; class Expression;
class Parameter; class Parameter;
class Type; class Type;
class TypeTuple;
struct OutBuffer; struct OutBuffer;
struct Target struct Target
...@@ -73,4 +74,5 @@ struct Target ...@@ -73,4 +74,5 @@ struct Target
static Type *cppParameterType(Parameter *p); static Type *cppParameterType(Parameter *p);
static bool cppFundamentalType(const Type *t, bool& isFundamental); static bool cppFundamentalType(const Type *t, bool& isFundamental);
static LINK systemLinkage(); static LINK systemLinkage();
static TypeTuple *toArgTypes(Type *t);
}; };
void chkArgTypes(S, V...)()
{
pragma(msg, S);
static if (is(S U == __argTypes))
{
foreach (T; U) { pragma(msg, T); }
static assert(U.length == V.length);
foreach (i, T; U)
static assert(is(V[i] == T));
}
else
static assert(0);
}
void chkSingle(T,U)()
{
struct S { T a; }
chkArgTypes!(S, U)();
}
void chkIdentity(T)()
{
chkSingle!(T,T)();
}
void chkPair(T,U,V)()
{
struct S { T a; U b; }
chkArgTypes!(S, V)();
}
version (X86_64)
{
int main()
{
chkIdentity!byte();
chkIdentity!ubyte();
chkIdentity!short();
chkIdentity!ushort();
chkIdentity!int();
chkIdentity!uint();
chkIdentity!long();
chkIdentity!ulong();
chkSingle!(char,ubyte)();
chkSingle!(wchar,ushort)();
chkSingle!(dchar,uint)();
chkIdentity!float();
chkIdentity!double();
chkIdentity!real();
chkIdentity!(void*)();
chkIdentity!(__vector(byte[16]))();
chkIdentity!(__vector(ubyte[16]))();
chkIdentity!(__vector(short[8]))();
chkIdentity!(__vector(ushort[8]))();
chkIdentity!(__vector(int[4]))();
chkIdentity!(__vector(uint[4]))();
chkIdentity!(__vector(long[2]))();
chkIdentity!(__vector(ulong[2]))();
chkIdentity!(__vector(float[4]))();
chkIdentity!(__vector(double[2]))();
chkPair!(byte,byte,short);
chkPair!(ubyte,ubyte,short);
chkPair!(short,short,int);
chkPair!(int,int,long);
chkPair!(byte,short,int);
chkPair!(short,byte,int);
chkPair!(int,float,long);
chkPair!(float,int,long);
chkPair!(byte,float,long);
chkPair!(float,short,long);
//struct S1 { long a; long b; }
//chkArgTypes!(S1, long, long)();
struct S2 { union { long a; double d; }}
chkArgTypes!(S2, long)();
struct S3 { union { double d; long a; }}
chkArgTypes!(S3, long)();
struct S4 { int a,b,c,d,e; }
chkArgTypes!(S4)();
struct S5 { align(1): char a; int b; }
chkArgTypes!(S5)();
struct S6 { align(1): int a; void* b; }
chkArgTypes!(S5)();
struct S7 { union { void* p; real r; }}
chkArgTypes!(S7)();
struct S8 { union { real r; void* p; }}
chkArgTypes!(S8)();
return 0;
}
}
else
{
int main()
{
return 0;
}
}
2020-04-13 Iain Buclaw <ibuclaw@gdcproject.org>
* libdruntime/core/stdc/stdarg.d: Remove run-time va_list template.
2020-04-10 Iain Buclaw <ibuclaw@gdcproject.org> 2020-04-10 Iain Buclaw <ibuclaw@gdcproject.org>
* d_rules.am (libdgruntime_la_LINK): Move to libdruntime/Makefile.am. * d_rules.am (libdgruntime_la_LINK): Move to libdruntime/Makefile.am.
......
...@@ -50,166 +50,6 @@ version (GNU) ...@@ -50,166 +50,6 @@ version (GNU)
void va_arg(T)(ref va_list ap, ref T parmn); void va_arg(T)(ref va_list ap, ref T parmn);
/*************
* Retrieve and store through parmn the next value that is of TypeInfo ti.
* Used when the static type is not known.
*/
version (X86)
{
///
void va_arg()(ref va_list ap, TypeInfo ti, void* parmn)
{
auto p = ap;
auto tsize = ti.tsize;
ap = cast(va_list)(cast(size_t)p + ((tsize + size_t.sizeof - 1) & ~(size_t.sizeof - 1)));
parmn[0..tsize] = p[0..tsize];
}
}
else version (X86_64)
{
/// Layout of this struct must match __builtin_va_list for C ABI compatibility
struct __va_list
{
uint offset_regs = 6 * 8; // no regs
uint offset_fpregs = 6 * 8 + 8 * 16; // no fp regs
void* stack_args;
void* reg_args;
}
///
void va_arg()(ref va_list apx, TypeInfo ti, void* parmn)
{
__va_list* ap = cast(__va_list*)apx;
TypeInfo arg1, arg2;
if (!ti.argTypes(arg1, arg2))
{
bool inXMMregister(TypeInfo arg) pure nothrow @safe
{
return (arg.flags & 2) != 0;
}
TypeInfo_Vector v1 = arg1 ? cast(TypeInfo_Vector)arg1 : null;
if (arg1 && (arg1.tsize() <= 8 || v1))
{ // Arg is passed in one register
auto tsize = arg1.tsize();
void* p;
bool stack = false;
auto offset_fpregs_save = ap.offset_fpregs;
auto offset_regs_save = ap.offset_regs;
L1:
if (inXMMregister(arg1) || v1)
{ // Passed in XMM register
if (ap.offset_fpregs < (6 * 8 + 16 * 8) && !stack)
{
p = ap.reg_args + ap.offset_fpregs;
ap.offset_fpregs += 16;
}
else
{
p = ap.stack_args;
ap.stack_args += (tsize + size_t.sizeof - 1) & ~(size_t.sizeof - 1);
stack = true;
}
}
else
{ // Passed in regular register
if (ap.offset_regs < 6 * 8 && !stack)
{
p = ap.reg_args + ap.offset_regs;
ap.offset_regs += 8;
}
else
{
p = ap.stack_args;
ap.stack_args += 8;
stack = true;
}
}
parmn[0..tsize] = p[0..tsize];
if (arg2)
{
if (inXMMregister(arg2))
{ // Passed in XMM register
if (ap.offset_fpregs < (6 * 8 + 16 * 8) && !stack)
{
p = ap.reg_args + ap.offset_fpregs;
ap.offset_fpregs += 16;
}
else
{
if (!stack)
{ // arg1 is really on the stack, so rewind and redo
ap.offset_fpregs = offset_fpregs_save;
ap.offset_regs = offset_regs_save;
stack = true;
goto L1;
}
p = ap.stack_args;
ap.stack_args += (arg2.tsize() + size_t.sizeof - 1) & ~(size_t.sizeof - 1);
}
}
else
{ // Passed in regular register
if (ap.offset_regs < 6 * 8 && !stack)
{
p = ap.reg_args + ap.offset_regs;
ap.offset_regs += 8;
}
else
{
if (!stack)
{ // arg1 is really on the stack, so rewind and redo
ap.offset_fpregs = offset_fpregs_save;
ap.offset_regs = offset_regs_save;
stack = true;
goto L1;
}
p = ap.stack_args;
ap.stack_args += 8;
}
}
auto sz = ti.tsize() - 8;
(parmn + 8)[0..sz] = p[0..sz];
}
}
else
{ // Always passed in memory
// The arg may have more strict alignment than the stack
auto talign = ti.talign();
auto tsize = ti.tsize();
auto p = cast(void*)((cast(size_t)ap.stack_args + talign - 1) & ~(talign - 1));
ap.stack_args = cast(void*)(cast(size_t)p + ((tsize + size_t.sizeof - 1) & ~(size_t.sizeof - 1)));
parmn[0..tsize] = p[0..tsize];
}
}
else
{
assert(false, "not a valid argument type for va_arg");
}
}
}
else version (ARM)
{
///
void va_arg()(ref va_list ap, TypeInfo ti, void* parmn)
{
auto p = *cast(void**) &ap;
auto tsize = ti.tsize();
*cast(void**) &ap += ( tsize + size_t.sizeof - 1 ) & ~( size_t.sizeof - 1 );
parmn[0..tsize] = p[0..tsize];
}
}
else
{
///
void va_arg()(ref va_list ap, TypeInfo ti, void* parmn)
{
static assert(false, "Unsupported platform");
}
}
/*********************** /***********************
* End use of ap. * End use of ap.
*/ */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment