Commit 7ffdb519 authored by dslomov's avatar dslomov Committed by Commit bot

[destructuring] Grand for statement parsing unification.

Also support patterns in ``for (var p in/of ...)``

This CL extends the rewriting we used to do for ``for (let p in/of...)`` to
``for (var p in/of ...)``. For all for..in/of loop declaring variable,
we rewrite
   for (var/let/const pattern in/of e) b
into
   for (x' in/of e) { var/let/const pattern = e; b }

This adds a small complication for debugger: for a statement
   for (var v in/of e) ...
we used to have
   var v;
   for (v in/of e) ...
and there was a separate breakpoint on ``var v`` line.
This breakpoint is actually useless since it is immediately followed by
a breakpoint on evaluation of ``e``, so this CL removes that breakpoint
location.

Similiraly, for let, it used to be that
  for (let v in/of e) ...
became
  for (x' in/of e) { let v; v  = x'; ... }
``let v``generetaed a useless breakpoint (with the location at the
loop's head. This CL removes that breakpoint as well.

R=arv@chromium.org,rossberg@chromium.org
BUG=v8:811
LOG=N

Review URL: https://codereview.chromium.org/1149043005

Cr-Commit-Position: refs/heads/master@{#28565}
parent a40e85d6
...@@ -2295,8 +2295,8 @@ const AstRawString* Parser::DeclarationParsingResult::SingleName() const { ...@@ -2295,8 +2295,8 @@ const AstRawString* Parser::DeclarationParsingResult::SingleName() const {
Block* Parser::DeclarationParsingResult::BuildInitializationBlock( Block* Parser::DeclarationParsingResult::BuildInitializationBlock(
ZoneList<const AstRawString*>* names, bool* ok) { ZoneList<const AstRawString*>* names, bool* ok) {
Block* result = Block* result = descriptor.parser->factory()->NewBlock(
descriptor.parser->factory()->NewBlock(NULL, 1, true, descriptor.pos); NULL, 1, true, descriptor.declaration_pos);
for (auto declaration : declarations) { for (auto declaration : declarations) {
PatternRewriter::DeclareAndInitializeVariables( PatternRewriter::DeclareAndInitializeVariables(
result, &descriptor, &declaration, names, CHECK_OK); result, &descriptor, &declaration, names, CHECK_OK);
...@@ -2350,7 +2350,8 @@ void Parser::ParseVariableDeclarations(VariableDeclarationContext var_context, ...@@ -2350,7 +2350,8 @@ void Parser::ParseVariableDeclarations(VariableDeclarationContext var_context,
// BindingPattern '=' AssignmentExpression // BindingPattern '=' AssignmentExpression
parsing_result->descriptor.parser = this; parsing_result->descriptor.parser = this;
parsing_result->descriptor.pos = peek_position(); parsing_result->descriptor.declaration_pos = peek_position();
parsing_result->descriptor.initialization_pos = peek_position();
parsing_result->descriptor.mode = VAR; parsing_result->descriptor.mode = VAR;
// True if the binding needs initialization. 'let' and 'const' declared // True if the binding needs initialization. 'let' and 'const' declared
// bindings are created uninitialized by their declaration nodes and // bindings are created uninitialized by their declaration nodes and
...@@ -3417,11 +3418,10 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels, ...@@ -3417,11 +3418,10 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels,
bool is_let_identifier_expression = false; bool is_let_identifier_expression = false;
DeclarationParsingResult parsing_result; DeclarationParsingResult parsing_result;
if (peek() != Token::SEMICOLON) { if (peek() != Token::SEMICOLON) {
if (peek() == Token::VAR || if (peek() == Token::VAR || peek() == Token::CONST ||
(peek() == Token::CONST && is_sloppy(language_mode()))) { (peek() == Token::LET && is_strict(language_mode()))) {
ParseVariableDeclarations(kForStatement, &parsing_result, CHECK_OK); ParseVariableDeclarations(kForStatement, &parsing_result, CHECK_OK);
Block* variable_statement = is_const = parsing_result.descriptor.mode == CONST;
parsing_result.BuildInitializationBlock(nullptr, CHECK_OK);
int num_decl = parsing_result.declarations.length(); int num_decl = parsing_result.declarations.length();
bool accept_IN = num_decl >= 1; bool accept_IN = num_decl >= 1;
...@@ -3454,124 +3454,100 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels, ...@@ -3454,124 +3454,100 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels,
*ok = false; *ok = false;
return nullptr; return nullptr;
} }
ForEachStatement* loop =
factory()->NewForEachStatement(mode, labels, stmt_pos);
Target target(&this->target_stack_, loop);
Expression* enumerable = ParseExpression(true, CHECK_OK); DCHECK(parsing_result.declarations.length() == 1);
Expect(Token::RPAREN, CHECK_OK); Block* init_block = nullptr;
VariableProxy* each = // special case for legacy for (var/const x =.... in)
scope_->NewUnresolved(factory(), parsing_result.SingleName(), if (is_sloppy(language_mode()) &&
Variable::NORMAL, each_beg_pos, each_end_pos); !IsLexicalVariableMode(parsing_result.descriptor.mode) &&
Statement* body = ParseSubStatement(NULL, CHECK_OK); parsing_result.declarations[0].initializer != nullptr) {
InitializeForEachStatement(loop, each, enumerable, body); VariableProxy* single_var = scope_->NewUnresolved(
Block* result = factory(), parsing_result.SingleName(), Variable::NORMAL,
factory()->NewBlock(NULL, 2, false, RelocInfo::kNoPosition); each_beg_pos, each_end_pos);
result->AddStatement(variable_statement, zone()); init_block = factory()->NewBlock(
result->AddStatement(loop, zone()); nullptr, 2, true, parsing_result.descriptor.declaration_pos);
scope_ = saved_scope; init_block->AddStatement(
for_scope->set_end_position(scanner()->location().end_pos); factory()->NewExpressionStatement(
for_scope = for_scope->FinalizeBlockScope(); factory()->NewAssignment(
DCHECK(for_scope == NULL); Token::ASSIGN, single_var,
// Parsed for-in loop w/ variable/const declaration. parsing_result.declarations[0].initializer,
return result; RelocInfo::kNoPosition),
} else { RelocInfo::kNoPosition),
init = variable_statement; zone());
}
} else if ((peek() == Token::LET || peek() == Token::CONST) &&
is_strict(language_mode())) {
is_const = peek() == Token::CONST;
ParseVariableDeclarations(kForStatement, &parsing_result, CHECK_OK);
DCHECK(parsing_result.descriptor.pos != RelocInfo::kNoPosition);
int num_decl = parsing_result.declarations.length();
bool accept_IN = num_decl >= 1;
bool accept_OF = true;
ForEachStatement::VisitMode mode;
int each_beg_pos = scanner()->location().beg_pos;
int each_end_pos = scanner()->location().end_pos;
if (accept_IN && CheckInOrOf(accept_OF, &mode, ok)) {
if (!*ok) return nullptr;
if (num_decl != 1) {
const char* loop_type =
mode == ForEachStatement::ITERATE ? "for-of" : "for-in";
ParserTraits::ReportMessageAt(
parsing_result.bindings_loc,
MessageTemplate::kForInOfLoopMultiBindings, loop_type);
*ok = false;
return nullptr;
}
if (parsing_result.first_initializer_loc.IsValid() &&
(is_strict(language_mode()) || mode == ForEachStatement::ITERATE)) {
if (mode == ForEachStatement::ITERATE) {
ReportMessageAt(parsing_result.first_initializer_loc,
MessageTemplate::kForOfLoopInitializer);
} else {
ReportMessageAt(parsing_result.first_initializer_loc,
MessageTemplate::kForInLoopInitializer);
}
*ok = false;
return nullptr;
} }
// Rewrite a for-in statement of the form
// Rewrite a for-in/of statement of the form
// //
// for (let/const x in e) b // for (let/const/var x in/of e) b
// //
// into // into
// //
// <let x' be a temporary variable> // <let x' be a temporary variable>
// for (x' in e) { // for (x' in/of e) {
// let/const x; // let/const/var x;
// x = x'; // x = x';
// b; // b;
// } // }
// TODO(keuchel): Move the temporary variable to the block scope, after
// implementing stack allocated block scoped variables.
Variable* temp = scope_->DeclarationScope()->NewTemporary( Variable* temp = scope_->DeclarationScope()->NewTemporary(
ast_value_factory()->dot_for_string()); ast_value_factory()->dot_for_string());
VariableProxy* temp_proxy =
factory()->NewVariableProxy(temp, each_beg_pos, each_end_pos);
ForEachStatement* loop = ForEachStatement* loop =
factory()->NewForEachStatement(mode, labels, stmt_pos); factory()->NewForEachStatement(mode, labels, stmt_pos);
Target target(&this->target_stack_, loop); Target target(&this->target_stack_, loop);
// The expression does not see the loop variable. // The expression does not see the lexical loop variables.
scope_ = saved_scope; scope_ = saved_scope;
Expression* enumerable = ParseExpression(true, CHECK_OK); Expression* enumerable = ParseExpression(true, CHECK_OK);
scope_ = for_scope; scope_ = for_scope;
Expect(Token::RPAREN, CHECK_OK); Expect(Token::RPAREN, CHECK_OK);
Statement* body = ParseSubStatement(NULL, CHECK_OK); Statement* body = ParseSubStatement(NULL, CHECK_OK);
Block* body_block = Block* body_block =
factory()->NewBlock(NULL, 3, false, RelocInfo::kNoPosition); factory()->NewBlock(NULL, 3, false, RelocInfo::kNoPosition);
auto each_initialization_block = factory()->NewBlock( auto each_initialization_block =
nullptr, 1, true, parsing_result.descriptor.pos); factory()->NewBlock(nullptr, 1, true, RelocInfo::kNoPosition);
{ {
DCHECK(parsing_result.declarations.length() == 1); DCHECK(parsing_result.declarations.length() == 1);
DeclarationParsingResult::Declaration decl = DeclarationParsingResult::Declaration decl =
parsing_result.declarations[0]; parsing_result.declarations[0];
decl.initializer = temp_proxy; auto descriptor = parsing_result.descriptor;
descriptor.declaration_pos = RelocInfo::kNoPosition;
decl.initializer = factory()->NewVariableProxy(temp);
PatternRewriter::DeclareAndInitializeVariables( PatternRewriter::DeclareAndInitializeVariables(
each_initialization_block, &parsing_result.descriptor, &decl, each_initialization_block, &descriptor, &decl,
&lexical_bindings, CHECK_OK); IsLexicalVariableMode(descriptor.mode) ? &lexical_bindings
: nullptr,
CHECK_OK);
} }
body_block->AddStatement(each_initialization_block, zone()); body_block->AddStatement(each_initialization_block, zone());
body_block->AddStatement(body, zone()); body_block->AddStatement(body, zone());
VariableProxy* temp_proxy =
factory()->NewVariableProxy(temp, each_beg_pos, each_end_pos);
InitializeForEachStatement(loop, temp_proxy, enumerable, body_block); InitializeForEachStatement(loop, temp_proxy, enumerable, body_block);
scope_ = saved_scope; scope_ = saved_scope;
for_scope->set_end_position(scanner()->location().end_pos); for_scope->set_end_position(scanner()->location().end_pos);
for_scope = for_scope->FinalizeBlockScope(); for_scope = for_scope->FinalizeBlockScope();
body_block->set_scope(for_scope); if (for_scope != nullptr) {
// Parsed for-in loop w/ let declaration. body_block->set_scope(for_scope);
return loop; }
// Parsed for-in loop w/ variable declarations.
if (init_block != nullptr) {
init_block->AddStatement(loop, zone());
return init_block;
} else {
return loop;
}
} else { } else {
init = parsing_result.BuildInitializationBlock(&lexical_bindings, init = parsing_result.BuildInitializationBlock(
CHECK_OK); IsLexicalVariableMode(parsing_result.descriptor.mode)
? &lexical_bindings
: nullptr,
CHECK_OK);
} }
} else { } else {
Scanner::Location lhs_location = scanner()->peek_location(); Scanner::Location lhs_location = scanner()->peek_location();
...@@ -3579,9 +3555,9 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels, ...@@ -3579,9 +3555,9 @@ Statement* Parser::ParseForStatement(ZoneList<const AstRawString*>* labels,
ForEachStatement::VisitMode mode; ForEachStatement::VisitMode mode;
bool accept_OF = expression->IsVariableProxy(); bool accept_OF = expression->IsVariableProxy();
is_let_identifier_expression = is_let_identifier_expression =
expression->IsVariableProxy() && expression->IsVariableProxy() &&
expression->AsVariableProxy()->raw_name() == expression->AsVariableProxy()->raw_name() ==
ast_value_factory()->let_string(); ast_value_factory()->let_string();
if (CheckInOrOf(accept_OF, &mode, ok)) { if (CheckInOrOf(accept_OF, &mode, ok)) {
if (!*ok) return nullptr; if (!*ok) return nullptr;
......
...@@ -950,7 +950,8 @@ class Parser : public ParserBase<ParserTraits> { ...@@ -950,7 +950,8 @@ class Parser : public ParserBase<ParserTraits> {
VariableMode mode; VariableMode mode;
bool is_const; bool is_const;
bool needs_init; bool needs_init;
int pos; int declaration_pos;
int initialization_pos;
Token::Value init_op; Token::Value init_op;
}; };
......
...@@ -51,7 +51,8 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -51,7 +51,8 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
const AstRawString* name = pattern->raw_name(); const AstRawString* name = pattern->raw_name();
VariableProxy* proxy = parser->NewUnresolved(name, descriptor_->mode); VariableProxy* proxy = parser->NewUnresolved(name, descriptor_->mode);
Declaration* declaration = factory()->NewVariableDeclaration( Declaration* declaration = factory()->NewVariableDeclaration(
proxy, descriptor_->mode, descriptor_->scope, descriptor_->pos); proxy, descriptor_->mode, descriptor_->scope,
descriptor_->declaration_pos);
Variable* var = parser->Declare(declaration, descriptor_->mode != VAR, ok_); Variable* var = parser->Declare(declaration, descriptor_->mode != VAR, ok_);
if (!*ok_) return; if (!*ok_) return;
DCHECK_NOT_NULL(var); DCHECK_NOT_NULL(var);
...@@ -126,7 +127,9 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -126,7 +127,9 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
ZoneList<Expression*>* arguments = ZoneList<Expression*>* arguments =
new (zone()) ZoneList<Expression*>(3, zone()); new (zone()) ZoneList<Expression*>(3, zone());
// We have at least 1 parameter. // We have at least 1 parameter.
arguments->Add(factory()->NewStringLiteral(name, descriptor_->pos), zone()); arguments->Add(
factory()->NewStringLiteral(name, descriptor_->declaration_pos),
zone());
CallRuntime* initialize; CallRuntime* initialize;
if (descriptor_->is_const) { if (descriptor_->is_const) {
...@@ -140,13 +143,14 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -140,13 +143,14 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
initialize = factory()->NewCallRuntime( initialize = factory()->NewCallRuntime(
ast_value_factory()->initialize_const_global_string(), ast_value_factory()->initialize_const_global_string(),
Runtime::FunctionForId(Runtime::kInitializeConstGlobal), arguments, Runtime::FunctionForId(Runtime::kInitializeConstGlobal), arguments,
descriptor_->pos); descriptor_->initialization_pos);
} else { } else {
// Add language mode. // Add language mode.
// We may want to pass singleton to avoid Literal allocations. // We may want to pass singleton to avoid Literal allocations.
LanguageMode language_mode = initialization_scope->language_mode(); LanguageMode language_mode = initialization_scope->language_mode();
arguments->Add( arguments->Add(factory()->NewNumberLiteral(language_mode,
factory()->NewNumberLiteral(language_mode, descriptor_->pos), zone()); descriptor_->declaration_pos),
zone());
// Be careful not to assign a value to the global variable if // Be careful not to assign a value to the global variable if
// we're in a with. The initialization value should not // we're in a with. The initialization value should not
...@@ -160,7 +164,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -160,7 +164,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
initialize = factory()->NewCallRuntime( initialize = factory()->NewCallRuntime(
ast_value_factory()->initialize_var_global_string(), ast_value_factory()->initialize_var_global_string(),
Runtime::FunctionForId(Runtime::kInitializeVarGlobal), arguments, Runtime::FunctionForId(Runtime::kInitializeVarGlobal), arguments,
descriptor_->pos); descriptor_->declaration_pos);
} else { } else {
initialize = NULL; initialize = NULL;
} }
...@@ -184,7 +188,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -184,7 +188,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
DCHECK_NOT_NULL(proxy->var()); DCHECK_NOT_NULL(proxy->var());
DCHECK_NOT_NULL(value); DCHECK_NOT_NULL(value);
Assignment* assignment = factory()->NewAssignment( Assignment* assignment = factory()->NewAssignment(
descriptor_->init_op, proxy, value, descriptor_->pos); descriptor_->init_op, proxy, value, descriptor_->initialization_pos);
block_->AddStatement( block_->AddStatement(
factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition), factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition),
zone()); zone());
...@@ -200,7 +204,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) { ...@@ -200,7 +204,7 @@ void Parser::PatternRewriter::VisitVariableProxy(VariableProxy* pattern) {
// property). // property).
VariableProxy* proxy = initialization_scope->NewUnresolved(factory(), name); VariableProxy* proxy = initialization_scope->NewUnresolved(factory(), name);
Assignment* assignment = factory()->NewAssignment( Assignment* assignment = factory()->NewAssignment(
descriptor_->init_op, proxy, value, descriptor_->pos); descriptor_->init_op, proxy, value, descriptor_->initialization_pos);
block_->AddStatement( block_->AddStatement(
factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition), factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition),
zone()); zone());
......
...@@ -6525,6 +6525,32 @@ TEST(DestructuringNegativeTests) { ...@@ -6525,6 +6525,32 @@ TEST(DestructuringNegativeTests) {
} }
TEST(DestructuringDisallowPatternsInForVarIn) {
i::FLAG_harmony_destructuring = true;
static const ParserFlag always_flags[] = {kAllowHarmonyDestructuring};
const char* context_data[][2] = {
{"", ""}, {"function f() {", "}"}, {NULL, NULL}};
// clang-format off
const char* error_data[] = {
"for (var {x} = {} in null);",
"for (var {x} = {} of null);",
"for (let x = {} in null);",
"for (let x = {} of null);",
NULL};
// clang-format on
RunParserSyncTest(context_data, error_data, kError, NULL, 0, always_flags,
arraysize(always_flags));
// clang-format off
const char* success_data[] = {
"for (var x = {} in null);",
NULL};
// clang-format on
RunParserSyncTest(context_data, success_data, kSuccess, NULL, 0, always_flags,
arraysize(always_flags));
}
TEST(SpreadArray) { TEST(SpreadArray) {
i::FLAG_harmony_spread_arrays = true; i::FLAG_harmony_spread_arrays = true;
......
...@@ -81,28 +81,28 @@ Debug.setListener(listener); ...@@ -81,28 +81,28 @@ Debug.setListener(listener);
f(); f();
Debug.setListener(null); // Break z Debug.setListener(null); // Break z
print(JSON.stringify(log)); print("log:\n"+ JSON.stringify(log));
// The let declaration differs from var in that the loop variable // The let declaration differs from var in that the loop variable
// is declared in every iteration. // is declared in every iteration.
var expected = [ var expected = [
// Entry // Entry
"a2","b2", "a2","b2",
// Empty for-in-var: var decl, get enumerable // Empty for-in-var: get enumerable
"c7","c16", "c16",
// Empty for-in: get enumerable // Empty for-in: get enumerable
"d12", "d12",
// For-in-var: var decl, get enumerable, assign, body, assign, body, ... // For-in-var: get enumerable, assign, body, assign, body, ...
"e7","e16","e11","E4","e11","E4","e11","E4","e11", "e16","e11","E4","e11","E4","e11","E4","e11",
// For-in: get enumerable, assign, body, assign, body, ... // For-in: get enumerable, assign, body, assign, body, ...
"f12","f7","F4","f7","F4","f7","F4","f7", "f12","f7","F4","f7","F4","f7","F4","f7",
// For-in-let: get enumerable, next, new let, body, next, new let, ... // For-in-let: get enumerable, next, body, next, ...
"g16","g11","g7","G4","g11","g7","G4","g11","g7","G4","g11", "g16","g11","G4","g11","G4","g11","G4","g11",
// For-of-var: var decl, next(), body, next(), body, ... // For-of-var: next(), body, next(), body, ...
"h7","h16","H4","h16","H4","h16","H4","h16", "h16","H4","h16","H4","h16","H4","h16",
// For-of: next(), body, next(), body, ... // For-of: next(), body, next(), body, ...
"i12","I4","i12","I4","i12","I4","i12", "i12","I4","i12","I4","i12","I4","i12",
// For-of-let: next(), new let, body, next(), new let, ... // For-of-let: next(), body, next(), ...
"j16","j7","J4","j16","j7","J4","j16","j7","J4","j16", "j16","J4","j16","J4","j16","J4","j16",
// For-var: var decl, condition, body, next, condition, body, ... // For-var: var decl, condition, body, next, condition, body, ...
"k7","k20","K4","k23","k20","K4","k23","k20","K4","k23","k20", "k7","k20","K4","k23","k20","K4","k23","k20","K4","k23","k20",
// For: init, condition, body, next, condition, body, ... // For: init, condition, body, next, condition, body, ...
...@@ -110,6 +110,7 @@ var expected = [ ...@@ -110,6 +110,7 @@ var expected = [
// Exit. // Exit.
"y0","z0", "y0","z0",
] ]
print("expected:\n"+ JSON.stringify(log));
assertArrayEquals(expected, log); assertArrayEquals(expected, log);
assertEquals(48, s); assertEquals(48, s);
......
...@@ -645,8 +645,7 @@ ...@@ -645,8 +645,7 @@
assertSame(-(i+1), fy()); assertSame(-(i+1), fy());
} }
var o = { 'a1':1, 'b2':2 }; var o = { __proto__:null, 'a1':1, 'b2':2 };
o.__proto__ = null;
let sx = ''; let sx = '';
let sy = ''; let sy = '';
for (let [x,y] in o) { for (let [x,y] in o) {
...@@ -656,3 +655,34 @@ ...@@ -656,3 +655,34 @@
assertEquals('ab', sx); assertEquals('ab', sx);
assertEquals('12', sy); assertEquals('12', sy);
}()); }());
(function TestForEachVars() {
var a = [{x:1, y:-1}, {x:2,y:-2}, {x:3,y:-3}];
var sumX = 0;
var sumY = 0;
var fs = [];
for (var {x,y} of a) {
sumX += x;
sumY += y;
fs.push({fx : function() { return x; }, fy : function() { return y }});
}
assertSame(6, sumX);
assertSame(-6, sumY);
assertSame(3, fs.length);
for (var i = 0; i < fs.length; i++) {
var {fx,fy} = fs[i];
assertSame(3, fx());
assertSame(-3, fy());
}
var o = { __proto__:null, 'a1':1, 'b2':2 };
var sx = '';
var sy = '';
for (var [x,y] in o) {
sx += x;
sy += y;
}
assertEquals('ab', sx);
assertEquals('12', sy);
}());
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