Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
Pyston
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
Pyston
Commits
b6ebc815
Commit
b6ebc815
authored
Apr 05, 2015
by
Marius Wachtler
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
LLVM: Add support for symbolic entries in the stackmap constant table
parent
23c27d2d
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
355 additions
and
109 deletions
+355
-109
llvm_patches/0011-Symbolic-Stackmap-Entries.patch
llvm_patches/0011-Symbolic-Stackmap-Entries.patch
+247
-0
src/codegen/stackmaps.cpp
src/codegen/stackmaps.cpp
+108
-109
No files found.
llvm_patches/0011-Symbolic-Stackmap-Entries.patch
0 → 100644
View file @
b6ebc815
From a27e2f111d85c2e55c5a9672b9ef39494b4d7fd8 Mon Sep 17 00:00:00 2001
From: Marius Wachtler <undingen@gmail.com>
Date: Wed, 15 Apr 2015 09:40:51 +0200
Subject: [PATCH] Add support for symbolic entries in the stackmap constant
table
---
include/llvm/CodeGen/StackMaps.h | 18 ++++++++++--
lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp | 9 ++++++
lib/CodeGen/StackMaps.cpp | 36 +++++++++++++++++++++---
test/CodeGen/X86/stackmap.ll | 26 +++++++++++++++--
4 files changed, 79 insertions(+), 10 deletions(-)
diff --git a/include/llvm/CodeGen/StackMaps.h b/include/llvm/CodeGen/StackMaps.h
index 4e48afe..30cae3b 100644
--- a/include/llvm/CodeGen/StackMaps.h
+++ b/include/llvm/CodeGen/StackMaps.h
@@ -136,9 +136,13 @@
public:
unsigned Size;
unsigned Reg;
int64_t Offset;
- Location() : LocType(Unprocessed), Size(0), Reg(0), Offset(0) {}
+ const MCSymbol *Sym;
+ Location() : LocType(Unprocessed), Size(0), Reg(0), Offset(0), Sym(0) {}
Location(LocationType LocType, unsigned Size, unsigned Reg, int64_t Offset)
- : LocType(LocType), Size(Size), Reg(Reg), Offset(Offset) {}
+ : LocType(LocType), Size(Size), Reg(Reg), Offset(Offset), Sym(0) {}
+ Location(const MCSymbol *Sym)
+ : LocType(LocationType::Constant), Size(sizeof(int64_t)), Reg(0),
+ Offset(0), Sym(Sym) {}
};
struct LiveOutReg {
@@ -160,13 +164,19 @@
public:
// OpTypes are used to encode information about the following logical
// operand (which may consist of several MachineOperands) for the
// OpParser.
- typedef enum { DirectMemRefOp, IndirectMemRefOp, ConstantOp } OpType;
+ typedef enum {
+ DirectMemRefOp,
+ IndirectMemRefOp,
+ ConstantOp,
+ ConstantGVOp
+ } OpType;
StackMaps(AsmPrinter &AP);
void reset() {
CSInfos.clear();
ConstPool.clear();
+ ConstSymPool.clear();
FnStackSize.clear();
}
@@ -191,6 +201,7 @@
private:
typedef SmallVector<Location, 8> LocationVec;
typedef SmallVector<LiveOutReg, 8> LiveOutVec;
typedef MapVector<uint64_t, uint64_t> ConstantPool;
+ typedef MapVector<const MCSymbol *, const MCSymbol *> ConstantSymMap;
typedef MapVector<const MCSymbol *, uint64_t> FnStackSizeMap;
struct CallsiteInfo {
@@ -210,6 +221,7 @@
private:
AsmPrinter &AP;
CallsiteInfoList CSInfos;
ConstantPool ConstPool;
+ ConstantSymMap ConstSymPool;
FnStackSizeMap FnStackSize;
MachineInstr::const_mop_iterator
diff --git a/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp b/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
index c0a8299..98cc692 100644
--- a/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
+++ b/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
@@ -7122,6 +7122,15 @@
static void addStackMapLiveVars(ImmutableCallSite CS, unsigned StartIdx,
const TargetLowering &TLI = Builder.DAG.getTargetLoweringInfo();
Ops.push_back(
Builder.DAG.getTargetFrameIndex(FI->getIndex(), TLI.getPointerTy()));
+ } else if (auto *GA = dyn_cast<GlobalAddressSDNode>(OpVal)) {
+ if (GA->getValueType(0) != MVT::i64)
+ Ops.push_back(OpVal);
+ else {
+ Ops.push_back(
+ Builder.DAG.getTargetConstant(StackMaps::ConstantGVOp, MVT::i64));
+ Ops.push_back(Builder.DAG.getTargetGlobalAddress(GA->getGlobal(),
+ SDLoc(GA), MVT::i64));
+ }
} else
Ops.push_back(OpVal);
}
diff --git a/lib/CodeGen/StackMaps.cpp b/lib/CodeGen/StackMaps.cpp
index 0797d56..7151166 100644
--- a/lib/CodeGen/StackMaps.cpp
+++ b/lib/CodeGen/StackMaps.cpp
@@ -107,6 +107,16 @@
StackMaps::parseOperand(MachineInstr::const_mop_iterator MOI,
Locs.push_back(Location(Location::Constant, sizeof(int64_t), 0, Imm));
break;
}
+ case StackMaps::ConstantGVOp: {
+ ++MOI;
+ assert(MOI->isGlobal() && "Expected a global value operand.");
+ const GlobalValue *GV = MOI->getGlobal();
+ assert(GV);
+ MCSymbol *Sym = AP.TM.getSymbol(GV, *AP.Mang);
+ assert(Sym);
+ Locs.push_back(Location(Sym));
+ break;
+ }
}
return ++MOI;
}
@@ -226,7 +236,7 @@
void StackMaps::recordStackMapOpers(const MachineInstr &MI, uint64_t ID,
I != E; ++I) {
// Constants are encoded as sign-extended integers.
// -1 is directly encoded as .long 0xFFFFFFFF with no constant pool.
- if (I->LocType == Location::Constant && !isInt<32>(I->Offset)) {
+ if (I->LocType == Location::Constant && !isInt<32>(I->Offset) && !I->Sym) {
I->LocType = Location::ConstantIndex;
// ConstPool is intentionally a MapVector of 'uint64_t's (as
// opposed to 'int64_t's). We should never be in a situation
@@ -243,6 +253,17 @@
void StackMaps::recordStackMapOpers(const MachineInstr &MI, uint64_t ID,
}
}
+ // Convert constant symbols to ConstantIndex entries.
+ for (LocationVec::iterator I = Locations.begin(), E = Locations.end(); I != E;
+ ++I) {
+ if (I->LocType == Location::Constant && I->Sym) {
+ I->LocType = Location::ConstantIndex;
+ auto Result = ConstSymPool.insert(std::make_pair(I->Sym, I->Sym));
+ // The symbolic entries will be emitted after the ConstPool entries.
+ I->Offset = ConstPool.size() + Result.first - ConstSymPool.begin();
+ }
+ }
+
// Create an expression to calculate the offset of the callsite from function
// entry.
const MCExpr *CSOffsetExpr = MCBinaryExpr::CreateSub(
@@ -325,8 +346,9 @@
void StackMaps::emitStackmapHeader(MCStreamer &OS) {
DEBUG(dbgs() << WSMP << "#functions = " << FnStackSize.size() << '\n');
OS.EmitIntValue(FnStackSize.size(), 4);
// Num constants.
- DEBUG(dbgs() << WSMP << "#constants = " << ConstPool.size() << '\n');
- OS.EmitIntValue(ConstPool.size(), 4);
+ auto NumConst = ConstPool.size() + ConstSymPool.size();
+ DEBUG(dbgs() << WSMP << "#constants = " << NumConst << '\n');
+ OS.EmitIntValue(NumConst, 4);
// Num callsites.
DEBUG(dbgs() << WSMP << "#callsites = " << CSInfos.size() << '\n');
OS.EmitIntValue(CSInfos.size(), 4);
@@ -359,6 +381,10 @@
void StackMaps::emitConstantPoolEntries(MCStreamer &OS) {
DEBUG(dbgs() << WSMP << ConstEntry.second << '\n');
OS.EmitIntValue(ConstEntry.second, 8);
}
+ for (auto ConstEntry : ConstSymPool) {
+ DEBUG(dbgs() << WSMP << ConstEntry.second << '\n');
+ OS.EmitSymbolValue(ConstEntry.second, 8);
+ }
}
/// Emit the callsite info for each callsite.
@@ -511,7 +537,8 @@
void StackMaps::emitCallsiteEntries(MCStreamer &OS,
void StackMaps::serializeToStackMapSection() {
(void) WSMP;
// Bail out if there's no stack map data.
- assert((!CSInfos.empty() || (CSInfos.empty() && ConstPool.empty())) &&
+ assert((!CSInfos.empty() ||
+ (CSInfos.empty() && ConstPool.empty() && ConstSymPool.empty())) &&
"Expected empty constant pool too!");
assert((!CSInfos.empty() || (CSInfos.empty() && FnStackSize.empty())) &&
"Expected empty function record too!");
@@ -541,4 +568,5 @@
void StackMaps::serializeToStackMapSection() {
// Clean up.
CSInfos.clear();
ConstPool.clear();
+ ConstSymPool.clear();
}
diff --git a/test/CodeGen/X86/stackmap.ll b/test/CodeGen/X86/stackmap.ll
index 5e356f3..59fa50f 100644
--- a/test/CodeGen/X86/stackmap.ll
+++ b/test/CodeGen/X86/stackmap.ll
@@ -11,7 +11,7 @@
; Num Functions
; CHECK-NEXT: .long 16
; Num LargeConstants
-; CHECK-NEXT: .long 3
+; CHECK-NEXT: .long 5
; Num Callsites
; CHECK-NEXT: .long 20
@@ -53,6 +53,8 @@
; CHECK-NEXT: .quad 2147483648
; CHECK-NEXT: .quad 4294967295
; CHECK-NEXT: .quad 4294967296
+; CHECK-NEXT: .quad _constSym1
+; CHECK-NEXT: .quad _constSym2
; Callsites
; Constant arguments
@@ -60,7 +62,7 @@
; CHECK-NEXT: .quad 1
; CHECK-NEXT: .long L{{.*}}-_constantargs
; CHECK-NEXT: .short 0
-; CHECK-NEXT: .short 12
+; CHECK-NEXT: .short 15
; SmallConstant
; CHECK-NEXT: .byte 4
; CHECK-NEXT: .byte 8
@@ -116,16 +118,34 @@
; CHECK-NEXT: .byte 8
; CHECK-NEXT: .short 0
; CHECK-NEXT: .long 2
+; LargeConstant at index 3
+; CHECK-NEXT: .byte 5
+; CHECK-NEXT: .byte 8
+; CHECK-NEXT: .short 0
+; CHECK-NEXT: .long 3
+; LargeConstant at index 3
+; CHECK-NEXT: .byte 5
+; CHECK-NEXT: .byte 8
+; CHECK-NEXT: .short 0
+; CHECK-NEXT: .long 3
+; LargeConstant at index 4
+; CHECK-NEXT: .byte 5
+; CHECK-NEXT: .byte 8
+; CHECK-NEXT: .short 0
+; CHECK-NEXT: .long 4
; SmallConstant
; CHECK-NEXT: .byte 4
; CHECK-NEXT: .byte 8
; CHECK-NEXT: .short 0
; CHECK-NEXT: .long -1
+
+@constSym1 = external constant i64
+@constSym2 = external constant i64
define void @constantargs() {
entry:
%0 = inttoptr i64 12345 to i8*
- tail call void (i64, i32, i8*, i32, ...)* @llvm.experimental.patchpoint.void(i64 1, i32 15, i8* %0, i32 0, i16 65535, i16 -1, i32 65536, i32 2000000000, i32 2147483647, i32 -1, i32 4294967295, i32 4294967296, i64 2147483648, i64 4294967295, i64 4294967296, i64 -1)
+ tail call void (i64, i32, i8*, i32, ...)* @llvm.experimental.patchpoint.void(i64 1, i32 15, i8* %0, i32 0, i16 65535, i16 -1, i32 65536, i32 2000000000, i32 2147483647, i32 -1, i32 4294967295, i32 4294967296, i64 2147483648, i64 4294967295, i64 4294967296, i64* @constSym1, i64* @constSym1, i64* @constSym2, i64 -1)
ret void
}
--
2.1.0
src/codegen/stackmaps.cpp
View file @
b6ebc815
...
...
@@ -32,11 +32,113 @@
namespace
pyston
{
// TODO shouldn't be recording this in a global variable
static
StackMap
*
cur_map
=
NULL
;
static
uint64_t
stackmap_address
=
0
;
StackMap
*
parseStackMap
()
{
StackMap
*
rtn
=
cur_map
;
cur_map
=
NULL
;
return
rtn
;
if
(
!
stackmap_address
)
return
NULL
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"Found the stackmaps at stackmap_address 0x%lx
\n
"
,
stackmap_address
);
StackMap
*
cur_map
=
new
StackMap
();
union
{
const
int8_t
*
i8
;
const
int16_t
*
i16
;
const
int32_t
*
i32
;
const
int64_t
*
i64
;
const
uint8_t
*
u8
;
const
uint16_t
*
u16
;
const
uint32_t
*
u32
;
const
uint64_t
*
u64
;
const
StackMap
::
Record
::
Location
*
record_loc
;
const
StackMap
::
Record
::
LiveOut
*
record_liveout
;
const
StackMap
::
StackSizeRecord
*
size_record
;
}
ptr
;
const
int8_t
*
start_ptr
=
ptr
.
i8
=
(
const
int8_t
*
)
stackmap_address
;
cur_map
->
header
=
*
ptr
.
u32
++
;
// header
#if LLVMREV < 200481
int
nfunctions
=
0
;
#else
int
nfunctions
=
*
ptr
.
u32
++
;
#endif
int
nconstants
=
*
ptr
.
u32
++
;
int
nrecords
=
*
ptr
.
u32
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d functions
\n
"
,
nfunctions
);
for
(
int
i
=
0
;
i
<
nfunctions
;
i
++
)
{
const
StackMap
::
StackSizeRecord
&
size_record
=
*
ptr
.
size_record
++
;
cur_map
->
stack_size_records
.
push_back
(
size_record
);
if
(
VERBOSITY
()
>=
3
)
printf
(
"function %d: offset 0x%lx, stack size 0x%lx
\n
"
,
i
,
size_record
.
offset
,
size_record
.
stack_size
);
}
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d constants
\n
"
,
nconstants
);
for
(
int
i
=
0
;
i
<
nconstants
;
i
++
)
{
uint64_t
constant
=
*
ptr
.
u64
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"Constant %d: %ld
\n
"
,
i
,
constant
);
cur_map
->
constants
.
push_back
(
constant
);
}
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d records
\n
"
,
nrecords
);
for
(
int
i
=
0
;
i
<
nrecords
;
i
++
)
{
StackMap
::
Record
*
record
=
new
StackMap
::
Record
();
cur_map
->
records
.
push_back
(
record
);
record
->
id
=
*
ptr
.
u64
++
;
record
->
offset
=
*
ptr
.
u32
++
;
record
->
flags
=
*
ptr
.
u16
++
;
// reserved (record flags)
int
numlocations
=
*
ptr
.
u16
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"Stackmap record %ld at 0x%x has %d locations:
\n
"
,
record
->
id
,
record
->
offset
,
numlocations
);
for
(
int
j
=
0
;
j
<
numlocations
;
j
++
)
{
assert
(
sizeof
(
StackMap
::
Record
::
Location
)
==
sizeof
(
*
ptr
.
u64
));
const
StackMap
::
Record
::
Location
&
r
=
*
ptr
.
record_loc
++
;
record
->
locations
.
push_back
(
r
);
// from http://lxr.free-electrons.com/source/tools/perf/arch/x86/util/dwarf-regs.c
// TODO this probably can be fetched more portably from the llvm target files
const
char
*
dwarf_reg_names
[]
=
{
"%rax"
,
"%rdx"
,
"%rcx"
,
"%rbx"
,
"%rsi"
,
"%rdi"
,
"%rbp"
,
"%rsp"
,
"%r8"
,
"%r9"
,
"%r10"
,
"%r11"
,
"%r12"
,
"%r13"
,
"%r14"
,
"%r15"
,
};
if
(
VERBOSITY
()
>=
3
)
{
if
(
r
.
type
==
1
)
{
printf
(
"Location %d: type %d (reg), reg %d (%s), offset %d
\n
"
,
j
,
r
.
type
,
r
.
regnum
,
dwarf_reg_names
[
r
.
regnum
],
r
.
offset
);
}
else
{
printf
(
"Location %d: type %d, reg %d, offset %d
\n
"
,
j
,
r
.
type
,
r
.
regnum
,
r
.
offset
);
}
}
}
ptr
.
u16
++
;
// padding
int
num_live_outs
=
*
ptr
.
u16
++
;
for
(
int
i
=
0
;
i
<
num_live_outs
;
i
++
)
{
const
StackMap
::
Record
::
LiveOut
&
r
=
*
ptr
.
record_liveout
++
;
record
->
live_outs
.
push_back
(
r
);
if
(
VERBOSITY
()
>=
3
)
{
printf
(
"Live out %d: reg #%d (?), size %d
\n
"
,
i
,
r
.
regnum
,
r
.
size
);
}
}
if
(
num_live_outs
%
2
==
0
)
ptr
.
u32
++
;
// pad to 8-byte boundary
}
stackmap_address
=
0
;
return
cur_map
;
}
class
StackmapJITEventListener
:
public
llvm
::
JITEventListener
{
...
...
@@ -68,112 +170,9 @@ void StackmapJITEventListener::NotifyObjectEmitted(const llvm::object::ObjectFil
assert
(
!
code
);
if
(
name
==
".llvm_stackmaps"
)
{
uint64_t
stackmap_address
=
L
.
getSectionLoadAddress
(
name
);
assert
(
stackmap_address
==
0
);
stackmap_address
=
L
.
getSectionLoadAddress
(
name
);
assert
(
stackmap_address
>
0
);
if
(
VERBOSITY
()
>=
3
)
printf
(
"Found the stackmaps at stackmap_address 0x%lx
\n
"
,
stackmap_address
);
assert
(
cur_map
==
NULL
);
cur_map
=
new
StackMap
();
union
{
const
int8_t
*
i8
;
const
int16_t
*
i16
;
const
int32_t
*
i32
;
const
int64_t
*
i64
;
const
uint8_t
*
u8
;
const
uint16_t
*
u16
;
const
uint32_t
*
u32
;
const
uint64_t
*
u64
;
const
StackMap
::
Record
::
Location
*
record_loc
;
const
StackMap
::
Record
::
LiveOut
*
record_liveout
;
const
StackMap
::
StackSizeRecord
*
size_record
;
}
ptr
;
const
int8_t
*
start_ptr
=
ptr
.
i8
=
(
const
int8_t
*
)
stackmap_address
;
cur_map
->
header
=
*
ptr
.
u32
++
;
// header
#if LLVMREV < 200481
int
nfunctions
=
0
;
#else
int
nfunctions
=
*
ptr
.
u32
++
;
#endif
int
nconstants
=
*
ptr
.
u32
++
;
int
nrecords
=
*
ptr
.
u32
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d functions
\n
"
,
nfunctions
);
for
(
int
i
=
0
;
i
<
nfunctions
;
i
++
)
{
const
StackMap
::
StackSizeRecord
&
size_record
=
*
ptr
.
size_record
++
;
cur_map
->
stack_size_records
.
push_back
(
size_record
);
if
(
VERBOSITY
()
>=
3
)
printf
(
"function %d: offset 0x%lx, stack size 0x%lx
\n
"
,
i
,
size_record
.
offset
,
size_record
.
stack_size
);
}
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d constants
\n
"
,
nconstants
);
for
(
int
i
=
0
;
i
<
nconstants
;
i
++
)
{
uint64_t
constant
=
*
ptr
.
u64
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"Constant %d: %ld
\n
"
,
i
,
constant
);
cur_map
->
constants
.
push_back
(
constant
);
}
if
(
VERBOSITY
()
>=
3
)
printf
(
"%d records
\n
"
,
nrecords
);
for
(
int
i
=
0
;
i
<
nrecords
;
i
++
)
{
StackMap
::
Record
*
record
=
new
StackMap
::
Record
();
cur_map
->
records
.
push_back
(
record
);
record
->
id
=
*
ptr
.
u64
++
;
record
->
offset
=
*
ptr
.
u32
++
;
record
->
flags
=
*
ptr
.
u16
++
;
// reserved (record flags)
int
numlocations
=
*
ptr
.
u16
++
;
if
(
VERBOSITY
()
>=
3
)
printf
(
"Stackmap record %ld at 0x%x has %d locations:
\n
"
,
record
->
id
,
record
->
offset
,
numlocations
);
for
(
int
j
=
0
;
j
<
numlocations
;
j
++
)
{
assert
(
sizeof
(
StackMap
::
Record
::
Location
)
==
sizeof
(
*
ptr
.
u64
));
const
StackMap
::
Record
::
Location
&
r
=
*
ptr
.
record_loc
++
;
record
->
locations
.
push_back
(
r
);
// from http://lxr.free-electrons.com/source/tools/perf/arch/x86/util/dwarf-regs.c
// TODO this probably can be fetched more portably from the llvm target files
const
char
*
dwarf_reg_names
[]
=
{
"%rax"
,
"%rdx"
,
"%rcx"
,
"%rbx"
,
"%rsi"
,
"%rdi"
,
"%rbp"
,
"%rsp"
,
"%r8"
,
"%r9"
,
"%r10"
,
"%r11"
,
"%r12"
,
"%r13"
,
"%r14"
,
"%r15"
,
};
if
(
VERBOSITY
()
>=
3
)
{
if
(
r
.
type
==
1
)
{
printf
(
"Location %d: type %d (reg), reg %d (%s), offset %d
\n
"
,
j
,
r
.
type
,
r
.
regnum
,
dwarf_reg_names
[
r
.
regnum
],
r
.
offset
);
}
else
{
printf
(
"Location %d: type %d, reg %d, offset %d
\n
"
,
j
,
r
.
type
,
r
.
regnum
,
r
.
offset
);
}
}
}
ptr
.
u16
++
;
// padding
int
num_live_outs
=
*
ptr
.
u16
++
;
for
(
int
i
=
0
;
i
<
num_live_outs
;
i
++
)
{
const
StackMap
::
Record
::
LiveOut
&
r
=
*
ptr
.
record_liveout
++
;
record
->
live_outs
.
push_back
(
r
);
if
(
VERBOSITY
()
>=
3
)
{
printf
(
"Live out %d: reg #%d (?), size %d
\n
"
,
i
,
r
.
regnum
,
r
.
size
);
}
}
if
(
num_live_outs
%
2
==
0
)
ptr
.
u32
++
;
// pad to 8-byte boundary
}
uint64_t
stackmap_size
=
sec
.
getSize
();
ASSERT
(
ptr
.
i8
-
start_ptr
==
stackmap_size
,
"%ld %ld"
,
ptr
.
i8
-
start_ptr
,
stackmap_size
);
}
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment