Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
cython
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
nexedi
cython
Commits
2675a89c
Commit
2675a89c
authored
Dec 02, 2011
by
Mark Florisson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow nested prange() & less warnings
parent
d532f26a
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
63 additions
and
115 deletions
+63
-115
Cython/Compiler/Nodes.py
Cython/Compiler/Nodes.py
+32
-5
Cython/Compiler/ParseTreeTransforms.py
Cython/Compiler/ParseTreeTransforms.py
+1
-1
Cython/Compiler/TypeInference.py
Cython/Compiler/TypeInference.py
+2
-4
tests/errors/e_cython_parallel.pyx
tests/errors/e_cython_parallel.pyx
+6
-3
tests/run/sequential_parallel.pyx
tests/run/sequential_parallel.pyx
+22
-102
No files found.
Cython/Compiler/Nodes.py
View file @
2675a89c
...
@@ -6834,10 +6834,11 @@ class ParallelStatNode(StatNode, ParallelNode):
...
@@ -6834,10 +6834,11 @@ class ParallelStatNode(StatNode, ParallelNode):
self
.
analyse_sharing_attributes
(
env
)
self
.
analyse_sharing_attributes
(
env
)
if
self
.
num_threads
is
not
None
:
if
self
.
num_threads
is
not
None
:
if
self
.
parent
and
self
.
parent
.
num_threads
is
not
None
:
if
(
self
.
parent
and
self
.
parent
.
num_threads
is
not
None
and
not
self
.
parent
.
is_prange
):
error
(
self
.
pos
,
error
(
self
.
pos
,
"num_threads already declared in outer section"
)
"num_threads already declared in outer section"
)
elif
self
.
parent
:
elif
self
.
parent
and
not
self
.
parent
.
is_prange
:
error
(
self
.
pos
,
error
(
self
.
pos
,
"num_threads must be declared in the parent parallel section"
)
"num_threads must be declared in the parent parallel section"
)
elif
(
self
.
num_threads
.
type
.
is_int
and
elif
(
self
.
num_threads
.
type
.
is_int
and
...
@@ -7113,11 +7114,15 @@ class ParallelStatNode(StatNode, ParallelNode):
...
@@ -7113,11 +7114,15 @@ class ParallelStatNode(StatNode, ParallelNode):
begin_code
=
self
.
begin_of_parallel_block
begin_code
=
self
.
begin_of_parallel_block
end_code
=
code
end_code
=
code
begin_code
.
putln
(
"#ifdef _OPENMP"
)
begin_code
.
put_ensure_gil
(
declare_gilstate
=
True
)
begin_code
.
put_ensure_gil
(
declare_gilstate
=
True
)
begin_code
.
putln
(
"Py_BEGIN_ALLOW_THREADS"
)
begin_code
.
putln
(
"Py_BEGIN_ALLOW_THREADS"
)
begin_code
.
putln
(
"#endif /* _OPENMP */"
)
end_code
.
putln
(
"#ifdef _OPENMP"
)
end_code
.
putln
(
"Py_END_ALLOW_THREADS"
)
end_code
.
putln
(
"Py_END_ALLOW_THREADS"
)
end_code
.
put_release_ensured_gil
()
end_code
.
put_release_ensured_gil
()
end_code
.
putln
(
"#endif /* _OPENMP */"
)
def
trap_parallel_exit
(
self
,
code
,
should_flush
=
False
):
def
trap_parallel_exit
(
self
,
code
,
should_flush
=
False
):
"""
"""
...
@@ -7216,8 +7221,13 @@ class ParallelStatNode(StatNode, ParallelNode):
...
@@ -7216,8 +7221,13 @@ class ParallelStatNode(StatNode, ParallelNode):
temp_count
+=
1
temp_count
+=
1
invalid_value
=
entry
.
type
.
invalid_value
()
if
invalid_value
:
init
=
' = '
+
invalid_value
else
:
init
=
''
# Declare the parallel private in the outer block
# Declare the parallel private in the outer block
c
.
putln
(
"%s %s
;"
%
(
type_decl
,
temp_cname
))
c
.
putln
(
"%s %s
%s;"
%
(
type_decl
,
temp_cname
,
init
))
# Initialize before escaping
# Initialize before escaping
code
.
putln
(
"%s = %s;"
%
(
temp_cname
,
private_cname
))
code
.
putln
(
"%s = %s;"
%
(
temp_cname
,
private_cname
))
...
@@ -7434,6 +7444,7 @@ class ParallelRangeNode(ParallelStatNode):
...
@@ -7434,6 +7444,7 @@ class ParallelRangeNode(ParallelStatNode):
start
=
stop
=
step
=
None
start
=
stop
=
step
=
None
is_prange
=
True
is_prange
=
True
is_nested_prange
=
False
nogil
=
None
nogil
=
None
schedule
=
None
schedule
=
None
...
@@ -7544,6 +7555,16 @@ class ParallelRangeNode(ParallelStatNode):
...
@@ -7544,6 +7555,16 @@ class ParallelRangeNode(ParallelStatNode):
if
self
.
nogil
:
if
self
.
nogil
:
env
.
nogil
=
was_nogil
env
.
nogil
=
was_nogil
self
.
is_nested_prange
=
self
.
parent
and
self
.
parent
.
is_prange
if
self
.
is_nested_prange
:
parent
=
self
while
parent
.
parent
and
parent
.
parent
.
is_prange
:
parent
=
parent
.
parent
parent
.
assignments
.
update
(
self
.
assignments
)
parent
.
privates
.
update
(
self
.
privates
)
parent
.
assigned_nodes
.
extend
(
self
.
assigned_nodes
)
def
nogil_check
(
self
,
env
):
def
nogil_check
(
self
,
env
):
names
=
'start'
,
'stop'
,
'step'
,
'target'
names
=
'start'
,
'stop'
,
'step'
,
'target'
nodes
=
self
.
start
,
self
.
stop
,
self
.
step
,
self
.
target
nodes
=
self
.
start
,
self
.
stop
,
self
.
step
,
self
.
target
...
@@ -7670,7 +7691,10 @@ class ParallelRangeNode(ParallelStatNode):
...
@@ -7670,7 +7691,10 @@ class ParallelRangeNode(ParallelStatNode):
self
.
release_closure_privates
(
code
)
self
.
release_closure_privates
(
code
)
def
generate_loop
(
self
,
code
,
fmt_dict
):
def
generate_loop
(
self
,
code
,
fmt_dict
):
code
.
putln
(
"#ifdef _OPENMP"
)
if
self
.
is_nested_prange
:
code
.
putln
(
"#if 0"
)
else
:
code
.
putln
(
"#ifdef _OPENMP"
)
if
not
self
.
is_parallel
:
if
not
self
.
is_parallel
:
code
.
put
(
"#pragma omp for"
)
code
.
put
(
"#pragma omp for"
)
...
@@ -7688,7 +7712,10 @@ class ParallelRangeNode(ParallelStatNode):
...
@@ -7688,7 +7712,10 @@ class ParallelRangeNode(ParallelStatNode):
# Initialize the GIL if needed for this thread
# Initialize the GIL if needed for this thread
self
.
begin_parallel_block
(
code
)
self
.
begin_parallel_block
(
code
)
code
.
putln
(
"#ifdef _OPENMP"
)
if
self
.
is_nested_prange
:
code
.
putln
(
"#if 0"
)
else
:
code
.
putln
(
"#ifdef _OPENMP"
)
code
.
put
(
"#pragma omp for"
)
code
.
put
(
"#pragma omp for"
)
for
entry
,
(
op
,
lastprivate
)
in
self
.
privates
.
iteritems
():
for
entry
,
(
op
,
lastprivate
)
in
self
.
privates
.
iteritems
():
...
...
Cython/Compiler/ParseTreeTransforms.py
View file @
2675a89c
...
@@ -1099,7 +1099,7 @@ class ParallelRangeTransform(CythonTransform, SkipDeclarations):
...
@@ -1099,7 +1099,7 @@ class ParallelRangeTransform(CythonTransform, SkipDeclarations):
if
isinstance
(
newnode
,
Nodes
.
ParallelWithBlockNode
):
if
isinstance
(
newnode
,
Nodes
.
ParallelWithBlockNode
):
if
self
.
state
==
'parallel with'
:
if
self
.
state
==
'parallel with'
:
error
(
node
.
manager
.
pos
,
error
(
node
.
manager
.
pos
,
"
Closely n
ested parallel with blocks are disallowed"
)
"
N
ested parallel with blocks are disallowed"
)
self
.
state
=
'parallel with'
self
.
state
=
'parallel with'
body
=
self
.
visit
(
node
.
body
)
body
=
self
.
visit
(
node
.
body
)
...
...
Cython/Compiler/TypeInference.py
View file @
2675a89c
...
@@ -196,10 +196,8 @@ class MarkAssignments(CythonTransform):
...
@@ -196,10 +196,8 @@ class MarkAssignments(CythonTransform):
self
.
parallel_block_stack
.
append
(
node
)
self
.
parallel_block_stack
.
append
(
node
)
nested
=
nested
or
len
(
self
.
parallel_block_stack
)
>
2
nested
=
nested
or
len
(
self
.
parallel_block_stack
)
>
2
if
not
self
.
parallel_errors
and
nested
:
if
not
self
.
parallel_errors
and
nested
and
not
node
.
is_prange
:
error
(
node
.
pos
,
"Only prange() may be nested"
)
error
(
node
.
pos
,
"Parallel nesting not supported due to bugs in gcc 4.5"
)
self
.
parallel_errors
=
True
self
.
parallel_errors
=
True
if
node
.
is_prange
:
if
node
.
is_prange
:
...
...
tests/errors/e_cython_parallel.pyx
View file @
2675a89c
...
@@ -145,6 +145,10 @@ cdef int chunksize():
...
@@ -145,6 +145,10 @@ cdef int chunksize():
for
i
in
prange
(
10
,
nogil
=
True
,
schedule
=
'static'
,
chunksize
=
chunksize
()):
for
i
in
prange
(
10
,
nogil
=
True
,
schedule
=
'static'
,
chunksize
=
chunksize
()):
pass
pass
with
nogil
,
cython
.
parallel
.
parallel
():
with
cython
.
parallel
.
parallel
():
pass
_ERRORS
=
u"""
_ERRORS
=
u"""
e_cython_parallel.pyx:3:8: cython.parallel.parallel is not a module
e_cython_parallel.pyx:3:8: cython.parallel.parallel is not a module
e_cython_parallel.pyx:4:0: No such directive: cython.parallel.something
e_cython_parallel.pyx:4:0: No such directive: cython.parallel.something
...
@@ -157,7 +161,7 @@ c_cython_parallel.pyx:21:29: The parallel section may only be used without the G
...
@@ -157,7 +161,7 @@ c_cython_parallel.pyx:21:29: The parallel section may only be used without the G
e_cython_parallel.pyx:27:10: target may not be a Python object as we don't have the GIL
e_cython_parallel.pyx:27:10: target may not be a Python object as we don't have the GIL
e_cython_parallel.pyx:30:9: Can only iterate over an iteration variable
e_cython_parallel.pyx:30:9: Can only iterate over an iteration variable
e_cython_parallel.pyx:33:10: Must be of numeric type, not int *
e_cython_parallel.pyx:33:10: Must be of numeric type, not int *
e_cython_parallel.pyx:36:33:
Closely n
ested parallel with blocks are disallowed
e_cython_parallel.pyx:36:33:
N
ested parallel with blocks are disallowed
e_cython_parallel.pyx:39:12: The parallel directive must be called
e_cython_parallel.pyx:39:12: The parallel directive must be called
e_cython_parallel.pyx:45:10: local variable 'y' referenced before assignment
e_cython_parallel.pyx:45:10: local variable 'y' referenced before assignment
e_cython_parallel.pyx:55:9: local variable 'y' referenced before assignment
e_cython_parallel.pyx:55:9: local variable 'y' referenced before assignment
...
@@ -166,8 +170,6 @@ e_cython_parallel.pyx:62:36: cython.parallel.parallel() does not take positional
...
@@ -166,8 +170,6 @@ e_cython_parallel.pyx:62:36: cython.parallel.parallel() does not take positional
e_cython_parallel.pyx:65:36: Invalid keyword argument: invalid
e_cython_parallel.pyx:65:36: Invalid keyword argument: invalid
e_cython_parallel.pyx:73:12: Yield not allowed in parallel sections
e_cython_parallel.pyx:73:12: Yield not allowed in parallel sections
e_cython_parallel.pyx:77:16: Yield not allowed in parallel sections
e_cython_parallel.pyx:77:16: Yield not allowed in parallel sections
e_cython_parallel.pyx:82:19: Parallel nesting not supported due to bugs in gcc 4.5
e_cython_parallel.pyx:87:23: Parallel nesting not supported due to bugs in gcc 4.5
e_cython_parallel.pyx:97:19: Cannot assign to private of outer parallel block
e_cython_parallel.pyx:97:19: Cannot assign to private of outer parallel block
e_cython_parallel.pyx:98:19: Cannot assign to private of outer parallel block
e_cython_parallel.pyx:98:19: Cannot assign to private of outer parallel block
e_cython_parallel.pyx:104:6: Reductions not allowed for parallel blocks
e_cython_parallel.pyx:104:6: Reductions not allowed for parallel blocks
...
@@ -180,4 +182,5 @@ e_cython_parallel.pyx:133:42: Must provide schedule with chunksize
...
@@ -180,4 +182,5 @@ e_cython_parallel.pyx:133:42: Must provide schedule with chunksize
e_cython_parallel.pyx:136:62: Chunksize must not be negative
e_cython_parallel.pyx:136:62: Chunksize must not be negative
e_cython_parallel.pyx:139:62: Chunksize not valid for the schedule runtime
e_cython_parallel.pyx:139:62: Chunksize not valid for the schedule runtime
e_cython_parallel.pyx:145:70: Calling gil-requiring function not allowed without gil
e_cython_parallel.pyx:145:70: Calling gil-requiring function not allowed without gil
e_cython_parallel.pyx:149:33: Nested parallel with blocks are disallowed
"""
"""
tests/run/sequential_parallel.pyx
View file @
2675a89c
...
@@ -46,7 +46,6 @@ def test_descending_prange():
...
@@ -46,7 +46,6 @@ def test_descending_prange():
return
sum
return
sum
'''
def
test_propagation
():
def
test_propagation
():
"""
"""
>>> test_propagation()
>>> test_propagation()
...
@@ -61,12 +60,10 @@ def test_propagation():
...
@@ -61,12 +60,10 @@ def test_propagation():
with
nogil
,
cython
.
parallel
.
parallel
():
with
nogil
,
cython
.
parallel
.
parallel
():
for
x
in
prange
(
10
):
for
x
in
prange
(
10
):
with cython.parallel.parallel():
for
y
in
prange
(
10
):
for y in prange(10):
sum2
+=
y
sum2 += y
return
i
,
j
,
x
,
y
,
sum1
,
sum2
return
i
,
j
,
x
,
y
,
sum1
,
sum2
'''
def
test_unsigned_operands
():
def
test_unsigned_operands
():
"""
"""
...
@@ -349,7 +346,6 @@ def test_prange_continue():
...
@@ -349,7 +346,6 @@ def test_prange_continue():
free
(
p
)
free
(
p
)
'''
def
test_nested_break_continue
():
def
test_nested_break_continue
():
"""
"""
>>> test_nested_break_continue()
>>> test_nested_break_continue()
...
@@ -371,16 +367,14 @@ def test_nested_break_continue():
...
@@ -371,16 +367,14 @@ def test_nested_break_continue():
print
i
,
j
,
result1
,
result2
print
i
,
j
,
result1
,
result2
with nogil, cython.parallel.parallel():
with
nogil
,
cython
.
parallel
.
parallel
(
num_threads
=
2
):
for i in prange(10, num_threads=2, schedule='static'):
for
i
in
prange
(
10
,
schedule
=
'static'
):
with cython.parallel.parallel():
if
i
==
8
:
if i == 8:
break
break
else
:
else:
continue
continue
print
i
print
i
'''
cdef
int
parallel_return
()
nogil
:
cdef
int
parallel_return
()
nogil
:
cdef
int
i
cdef
int
i
...
@@ -400,11 +394,10 @@ def test_return():
...
@@ -400,11 +394,10 @@ def test_return():
"""
"""
print
parallel_return
()
print
parallel_return
()
'''
def
test_parallel_exceptions
():
def
test_parallel_exceptions
():
"""
"""
>>> test_parallel_exceptions()
>>> test_parallel_exceptions()
('I am executed first', 0)
I am executed first
('propagate me',) 0
('propagate me',) 0
"""
"""
cdef
int
i
,
j
,
sum
=
0
cdef
int
i
,
j
,
sum
=
0
...
@@ -422,11 +415,10 @@ def test_parallel_exceptions():
...
@@ -422,11 +415,10 @@ def test_parallel_exceptions():
sum
+=
i
sum
+=
i
finally
:
finally
:
with
gil
:
with
gil
:
mylist.append(
("I am executed first", sum)
)
mylist
.
append
(
"I am executed first"
)
except
Exception
,
e
:
except
Exception
,
e
:
print
mylist
[
0
]
print
mylist
[
0
]
print
e
.
args
,
sum
print
e
.
args
,
sum
'''
def
test_parallel_exceptions_unnested
():
def
test_parallel_exceptions_unnested
():
"""
"""
...
@@ -453,7 +445,6 @@ def test_parallel_exceptions_unnested():
...
@@ -453,7 +445,6 @@ def test_parallel_exceptions_unnested():
print
mylist
[
0
]
print
mylist
[
0
]
print
e
.
args
,
sum
print
e
.
args
,
sum
'''
cdef
int
parallel_exc_cdef
()
except
-
3
:
cdef
int
parallel_exc_cdef
()
except
-
3
:
cdef
int
i
,
j
cdef
int
i
,
j
for
i
in
prange
(
10
,
nogil
=
True
):
for
i
in
prange
(
10
,
nogil
=
True
):
...
@@ -462,7 +453,6 @@ cdef int parallel_exc_cdef() except -3:
...
@@ -462,7 +453,6 @@ cdef int parallel_exc_cdef() except -3:
raise
Exception
(
"propagate me"
)
raise
Exception
(
"propagate me"
)
return
0
return
0
'''
cdef
int
parallel_exc_cdef_unnested
()
except
-
3
:
cdef
int
parallel_exc_cdef_unnested
()
except
-
3
:
cdef
int
i
cdef
int
i
...
@@ -480,9 +470,8 @@ def test_parallel_exc_cdef():
...
@@ -480,9 +470,8 @@ def test_parallel_exc_cdef():
Exception: propagate me
Exception: propagate me
"""
"""
parallel_exc_cdef_unnested
()
parallel_exc_cdef_unnested
()
#
parallel_exc_cdef()
parallel_exc_cdef
()
'''
cpdef
int
parallel_exc_cpdef
()
except
-
3
:
cpdef
int
parallel_exc_cpdef
()
except
-
3
:
cdef
int
i
,
j
cdef
int
i
,
j
for
i
in
prange
(
10
,
nogil
=
True
):
for
i
in
prange
(
10
,
nogil
=
True
):
...
@@ -491,7 +480,6 @@ cpdef int parallel_exc_cpdef() except -3:
...
@@ -491,7 +480,6 @@ cpdef int parallel_exc_cpdef() except -3:
raise
Exception
(
"propagate me"
)
raise
Exception
(
"propagate me"
)
return
0
return
0
'''
cpdef
int
parallel_exc_cpdef_unnested
()
except
-
3
:
cpdef
int
parallel_exc_cpdef_unnested
()
except
-
3
:
cdef
int
i
,
j
cdef
int
i
,
j
...
@@ -510,9 +498,8 @@ def test_parallel_exc_cpdef():
...
@@ -510,9 +498,8 @@ def test_parallel_exc_cpdef():
Exception: propagate me
Exception: propagate me
"""
"""
parallel_exc_cpdef_unnested
()
parallel_exc_cpdef_unnested
()
#
parallel_exc_cpdef()
parallel_exc_cpdef
()
'''
cdef
int
parallel_exc_nogil_swallow
()
except
-
1
:
cdef
int
parallel_exc_nogil_swallow
()
except
-
1
:
cdef
int
i
,
j
cdef
int
i
,
j
for
i
in
prange
(
10
,
nogil
=
True
):
for
i
in
prange
(
10
,
nogil
=
True
):
...
@@ -524,7 +511,6 @@ cdef int parallel_exc_nogil_swallow() except -1:
...
@@ -524,7 +511,6 @@ cdef int parallel_exc_nogil_swallow() except -1:
return
i
return
i
return
0
return
0
'''
cdef
int
parallel_exc_nogil_swallow_unnested
()
except
-
1
:
cdef
int
parallel_exc_nogil_swallow_unnested
()
except
-
1
:
cdef
int
i
cdef
int
i
...
@@ -542,13 +528,13 @@ def test_parallel_exc_nogil_swallow():
...
@@ -542,13 +528,13 @@ def test_parallel_exc_nogil_swallow():
"""
"""
>>> test_parallel_exc_nogil_swallow()
>>> test_parallel_exc_nogil_swallow()
execute me
execute me
execute me
"""
"""
parallel_exc_nogil_swallow_unnested
()
parallel_exc_nogil_swallow_unnested
()
print
'execute me'
print
'execute me'
#
parallel_exc_nogil_swallow()
parallel_exc_nogil_swallow
()
#
print 'execute me'
print
'execute me'
'''
def
parallel_exc_replace
():
def
parallel_exc_replace
():
"""
"""
>>> parallel_exc_replace()
>>> parallel_exc_replace()
...
@@ -569,7 +555,13 @@ def parallel_exc_replace():
...
@@ -569,7 +555,13 @@ def parallel_exc_replace():
return
0
return
0
def _parallel_exceptions2():
def
parallel_exceptions2
():
"""
>>> parallel_exceptions2()
Traceback (most recent call last):
...
Exception: propagate me
"""
cdef
int
i
,
j
,
k
cdef
int
i
,
j
,
k
for
i
in
prange
(
10
,
nogil
=
True
):
for
i
in
prange
(
10
,
nogil
=
True
):
...
@@ -582,59 +574,6 @@ def _parallel_exceptions2():
...
@@ -582,59 +574,6 @@ def _parallel_exceptions2():
continue
continue
return
return
def test_parallel_exceptions2():
"""
DISABLED
test_parallel_exceptions2()
read: start
propagate me
exiting...
Exit status: 0
"""
if not hasattr(os, 'fork'):
print 'start'
print 'propagate me'
print 'Exit status: 0'
return
r, w = os.pipe()
fr = os.fdopen(r, 'r')
fw = os.fdopen(w, 'w', 0)
pid = os.fork()
if pid == 0:
try:
fr.close()
os.dup2(w, 1)
os.dup2(w, 2)
print >>fw, 'start'
try:
_parallel_exceptions2()
except Exception, e:
print >>fw, e.args[0]
else:
print >>fw, 'No exception caught'
except:
import traceback
print >>fw, traceback.format_exc()
finally:
print >>fw, 'exiting...'
os._exit(0)
else:
fw.close()
print 'read:', fr.read(),
pid, status = os.waitpid(pid, 0)
if os.WIFSIGNALED(status):
print 'Got signal', os.WTERMSIG(status)
print 'Exit status:', os.WEXITSTATUS(status)
'''
def
test_parallel_with_gil_return
():
def
test_parallel_with_gil_return
():
"""
"""
>>> test_parallel_with_gil_return()
>>> test_parallel_with_gil_return()
...
@@ -654,25 +593,6 @@ def test_parallel_with_gil_return():
...
@@ -654,25 +593,6 @@ def test_parallel_with_gil_return():
with
gil
:
with
gil
:
return
sum
return
sum
'''
def test_parallel_with_gil_continue():
"""
>>> test_parallel_with_gil_continue()
20
"""
cdef int i, sum = 0
for i in prange(10, nogil=True):
with cython.parallel.parallel():
with gil:
if i % 2:
continue
sum += i
print sum
'''
def
test_parallel_with_gil_continue_unnested
():
def
test_parallel_with_gil_continue_unnested
():
"""
"""
>>> test_parallel_with_gil_continue_unnested()
>>> test_parallel_with_gil_continue_unnested()
...
...
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