Commit c2369e9e authored by marja's avatar marja Committed by Commit bot

Move ParseVariableDeclarations to ParserBase.

This enables PreParser to declare variables in the future without
duplicating the parsing logic.

BUG=

Review-Url: https://codereview.chromium.org/2297563007
Cr-Commit-Position: refs/heads/master@{#39079}
parent 281e4d0e
......@@ -214,6 +214,7 @@ class ParserBase {
typedef typename Types::StatementList StatementListT;
typedef typename v8::internal::ExpressionClassifier<Types>
ExpressionClassifier;
typedef typename Types::Block BlockT;
// All implementation-specific methods must be called through this.
Impl* impl() { return static_cast<Impl*>(this); }
......@@ -1177,6 +1178,11 @@ class ParserBase {
bool has_rest, int formals_start_pos,
int formals_end_pos, bool* ok);
BlockT ParseVariableDeclarations(VariableDeclarationContext var_context,
DeclarationParsingResult* parsing_result,
ZoneList<const AstRawString*>* names,
bool* ok);
bool IsNextLetKeyword();
bool IsTrivialExpression();
......@@ -3409,6 +3415,154 @@ void ParserBase<Impl>::ParseFormalParameterList(FormalParametersT* parameters,
}
}
template <typename Impl>
typename ParserBase<Impl>::BlockT ParserBase<Impl>::ParseVariableDeclarations(
VariableDeclarationContext var_context,
DeclarationParsingResult* parsing_result,
ZoneList<const AstRawString*>* names, bool* ok) {
// VariableDeclarations ::
// ('var' | 'const' | 'let') (Identifier ('=' AssignmentExpression)?)+[',']
//
// ES6:
// FIXME(marja, nikolaos): Add an up-to-date comment about ES6 variable
// declaration syntax.
DeclarationParsingResult temp_result;
if (parsing_result == nullptr) {
parsing_result = &temp_result;
}
parsing_result->descriptor.declaration_kind = DeclarationDescriptor::NORMAL;
parsing_result->descriptor.declaration_pos = peek_position();
parsing_result->descriptor.initialization_pos = peek_position();
BlockT init_block = impl()->NullBlock();
if (var_context != kForStatement) {
init_block = impl()->NewBlock(nullptr, 1, true,
parsing_result->descriptor.declaration_pos);
}
switch (peek()) {
case Token::VAR:
parsing_result->descriptor.mode = VAR;
Consume(Token::VAR);
break;
case Token::CONST:
Consume(Token::CONST);
DCHECK(var_context != kStatement);
parsing_result->descriptor.mode = CONST;
break;
case Token::LET:
Consume(Token::LET);
DCHECK(var_context != kStatement);
parsing_result->descriptor.mode = LET;
break;
default:
UNREACHABLE(); // by current callers
break;
}
parsing_result->descriptor.scope = scope();
parsing_result->descriptor.hoist_scope = nullptr;
// The scope of a var/const declared variable anywhere inside a function
// is the entire function (ECMA-262, 3rd, 10.1.3, and 12.2). The scope
// of a let declared variable is the scope of the immediately enclosing
// block.
int bindings_start = peek_position();
do {
// Parse binding pattern.
FuncNameInferrer::State fni_state(fni_);
ExpressionT pattern = impl()->EmptyExpression();
int decl_pos = peek_position();
{
ExpressionClassifier pattern_classifier(this);
pattern = ParsePrimaryExpression(CHECK_OK_CUSTOM(NullBlock));
ValidateBindingPattern(CHECK_OK_CUSTOM(NullBlock));
if (IsLexicalVariableMode(parsing_result->descriptor.mode)) {
ValidateLetPattern(CHECK_OK_CUSTOM(NullBlock));
}
}
Scanner::Location variable_loc = scanner()->location();
bool single_name = impl()->IsIdentifier(pattern);
if (single_name && fni_ != nullptr) {
impl()->PushVariableName(fni_, impl()->AsIdentifier(pattern));
}
ExpressionT value = impl()->EmptyExpression();
int initializer_position = kNoSourcePosition;
if (Check(Token::ASSIGN)) {
ExpressionClassifier classifier(this);
value = ParseAssignmentExpression(var_context != kForStatement,
CHECK_OK_CUSTOM(NullBlock));
impl()->RewriteNonPattern(CHECK_OK_CUSTOM(NullBlock));
variable_loc.end_pos = scanner()->location().end_pos;
if (!parsing_result->first_initializer_loc.IsValid()) {
parsing_result->first_initializer_loc = variable_loc;
}
// Don't infer if it is "a = function(){...}();"-like expression.
if (single_name && fni_ != nullptr) {
if (!value->IsCall() && !value->IsCallNew()) {
fni_->Infer();
} else {
fni_->RemoveLastFunction();
}
}
impl()->SetFunctionNameFromIdentifierRef(value, pattern);
// End position of the initializer is after the assignment expression.
initializer_position = scanner()->location().end_pos;
} else {
if (var_context != kForStatement || !PeekInOrOf()) {
// ES6 'const' and binding patterns require initializers.
if (parsing_result->descriptor.mode == CONST ||
!impl()->IsIdentifier(pattern)) {
impl()->ReportMessageAt(
Scanner::Location(decl_pos, scanner()->location().end_pos),
MessageTemplate::kDeclarationMissingInitializer,
!impl()->IsIdentifier(pattern) ? "destructuring" : "const");
*ok = false;
return impl()->NullBlock();
}
// 'let x' initializes 'x' to undefined.
if (parsing_result->descriptor.mode == LET) {
value = impl()->GetLiteralUndefined(position());
}
}
// End position of the initializer is after the variable.
initializer_position = position();
}
typename DeclarationParsingResult::Declaration decl(
pattern, initializer_position, value);
if (var_context == kForStatement) {
// Save the declaration for further handling in ParseForStatement.
parsing_result->declarations.Add(decl);
} else {
// Immediately declare the variable otherwise. This avoids O(N^2)
// behavior (where N is the number of variables in a single
// declaration) in the PatternRewriter having to do with removing
// and adding VariableProxies to the Scope (see bug 4699).
impl()->DeclareAndInitializeVariables(init_block,
&parsing_result->descriptor, &decl,
names, CHECK_OK_CUSTOM(NullBlock));
}
} while (Check(Token::COMMA));
parsing_result->bindings_loc =
Scanner::Location(bindings_start, scanner()->location().end_pos);
DCHECK(*ok);
return init_block;
}
template <typename Impl>
void ParserBase<Impl>::CheckArityRestrictions(int param_count,
FunctionKind function_kind,
......
......@@ -1904,6 +1904,14 @@ Block* Parser::BuildInitializationBlock(
return result;
}
void Parser::DeclareAndInitializeVariables(
Block* block, const DeclarationDescriptor* declaration_descriptor,
const DeclarationParsingResult::Declaration* declaration,
ZoneList<const AstRawString*>* names, bool* ok) {
DCHECK_NOT_NULL(block);
PatternRewriter::DeclareAndInitializeVariables(
this, block, declaration_descriptor, declaration, names, ok);
}
Block* Parser::ParseVariableStatement(VariableDeclarationContext var_context,
ZoneList<const AstRawString*>* names,
......@@ -1930,157 +1938,6 @@ Block* Parser::ParseVariableStatement(VariableDeclarationContext var_context,
return result;
}
Block* Parser::ParseVariableDeclarations(
VariableDeclarationContext var_context,
DeclarationParsingResult* parsing_result,
ZoneList<const AstRawString*>* names, bool* ok) {
// VariableDeclarations ::
// ('var' | 'const' | 'let') (Identifier ('=' AssignmentExpression)?)+[',']
//
// The ES6 Draft Rev3 specifies the following grammar for const declarations
//
// ConstDeclaration ::
// const ConstBinding (',' ConstBinding)* ';'
// ConstBinding ::
// Identifier '=' AssignmentExpression
//
// TODO(ES6):
// ConstBinding ::
// BindingPattern '=' AssignmentExpression
parsing_result->descriptor.declaration_kind = DeclarationDescriptor::NORMAL;
parsing_result->descriptor.declaration_pos = peek_position();
parsing_result->descriptor.initialization_pos = peek_position();
parsing_result->descriptor.mode = VAR;
Block* init_block = nullptr;
if (var_context != kForStatement) {
init_block = factory()->NewBlock(
NULL, 1, true, parsing_result->descriptor.declaration_pos);
}
if (peek() == Token::VAR) {
Consume(Token::VAR);
} else if (peek() == Token::CONST) {
Consume(Token::CONST);
DCHECK(var_context != kStatement);
parsing_result->descriptor.mode = CONST;
} else if (peek() == Token::LET) {
Consume(Token::LET);
DCHECK(var_context != kStatement);
parsing_result->descriptor.mode = LET;
} else {
UNREACHABLE(); // by current callers
}
parsing_result->descriptor.scope = scope();
parsing_result->descriptor.hoist_scope = nullptr;
bool first_declaration = true;
int bindings_start = peek_position();
do {
FuncNameInferrer::State fni_state(fni_);
// Parse name.
if (!first_declaration) Consume(Token::COMMA);
Expression* pattern;
int decl_pos = peek_position();
{
ExpressionClassifier pattern_classifier(this);
pattern = ParsePrimaryExpression(CHECK_OK);
ValidateBindingPattern(CHECK_OK);
if (IsLexicalVariableMode(parsing_result->descriptor.mode)) {
ValidateLetPattern(CHECK_OK);
}
}
Scanner::Location variable_loc = scanner()->location();
const AstRawString* single_name =
pattern->IsVariableProxy() ? pattern->AsVariableProxy()->raw_name()
: nullptr;
if (single_name != nullptr) {
if (fni_ != NULL) fni_->PushVariableName(single_name);
}
Expression* value = NULL;
int initializer_position = kNoSourcePosition;
if (Check(Token::ASSIGN)) {
ExpressionClassifier classifier(this);
value = ParseAssignmentExpression(var_context != kForStatement, CHECK_OK);
RewriteNonPattern(CHECK_OK);
variable_loc.end_pos = scanner()->location().end_pos;
if (!parsing_result->first_initializer_loc.IsValid()) {
parsing_result->first_initializer_loc = variable_loc;
}
// Don't infer if it is "a = function(){...}();"-like expression.
if (single_name) {
if (fni_ != NULL && value->AsCall() == NULL &&
value->AsCallNew() == NULL) {
fni_->Infer();
} else {
fni_->RemoveLastFunction();
}
}
SetFunctionNameFromIdentifierRef(value, pattern);
// End position of the initializer is after the assignment expression.
initializer_position = scanner()->location().end_pos;
} else {
// Initializers may be either required or implied unless this is a
// for-in/of iteration variable.
if (var_context != kForStatement || !PeekInOrOf()) {
// ES6 'const' and binding patterns require initializers.
if (parsing_result->descriptor.mode == CONST ||
!pattern->IsVariableProxy()) {
ReportMessageAt(
Scanner::Location(decl_pos, scanner()->location().end_pos),
MessageTemplate::kDeclarationMissingInitializer,
!pattern->IsVariableProxy() ? "destructuring" : "const");
*ok = false;
return nullptr;
}
// 'let x' initializes 'x' to undefined.
if (parsing_result->descriptor.mode == LET) {
value = GetLiteralUndefined(position());
}
}
// End position of the initializer is after the variable.
initializer_position = position();
}
DeclarationParsingResult::Declaration decl(pattern, initializer_position,
value);
if (var_context == kForStatement) {
// Save the declaration for further handling in ParseForStatement.
parsing_result->declarations.Add(decl);
} else {
// Immediately declare the variable otherwise. This avoids O(N^2)
// behavior (where N is the number of variables in a single
// declaration) in the PatternRewriter having to do with removing
// and adding VariableProxies to the Scope (see bug 4699).
DCHECK_NOT_NULL(init_block);
PatternRewriter::DeclareAndInitializeVariables(
this, init_block, &parsing_result->descriptor, &decl, names,
CHECK_OK);
}
first_declaration = false;
} while (peek() == Token::COMMA);
parsing_result->bindings_loc =
Scanner::Location(bindings_start, scanner()->location().end_pos);
DCHECK(*ok);
return init_block;
}
static bool ContainsLabel(ZoneList<const AstRawString*>* labels,
const AstRawString* label) {
DCHECK(label != NULL);
......
......@@ -159,6 +159,7 @@ struct ParserTypes<Parser> {
typedef ParserFormalParameters::Parameter FormalParameter;
typedef ParserFormalParameters FormalParameters;
typedef ZoneList<v8::internal::Statement*>* StatementList;
typedef v8::internal::Block* Block;
// For constructing objects returned by the traversing functions.
typedef AstNodeFactory Factory;
......@@ -295,6 +296,10 @@ class Parser : public ParserBase<Parser> {
Block* BuildInitializationBlock(DeclarationParsingResult* parsing_result,
ZoneList<const AstRawString*>* names,
bool* ok);
void DeclareAndInitializeVariables(
Block* block, const DeclarationDescriptor* declaration_descriptor,
const DeclarationParsingResult::Declaration* declaration,
ZoneList<const AstRawString*>* names, bool* ok);
Block* ParseVariableStatement(VariableDeclarationContext var_context,
ZoneList<const AstRawString*>* names,
......@@ -383,10 +388,6 @@ class Parser : public ParserBase<Parser> {
DEFINE_AST_VISITOR_MEMBERS_WITHOUT_STACKOVERFLOW()
};
Block* ParseVariableDeclarations(VariableDeclarationContext var_context,
DeclarationParsingResult* parsing_result,
ZoneList<const AstRawString*>* names,
bool* ok);
Statement* ParseExpressionOrLabelledStatement(
ZoneList<const AstRawString*>* labels,
AllowLabelledFunctionStatement allow_function, bool* ok);
......@@ -719,6 +720,11 @@ class Parser : public ParserBase<Parser> {
fni->PushLiteralName(id);
}
V8_INLINE static void PushVariableName(FuncNameInferrer* fni,
const AstRawString* id) {
fni->PushVariableName(id);
}
V8_INLINE void PushPropertyName(FuncNameInferrer* fni,
Expression* expression) {
if (expression->IsPropertyName()) {
......@@ -838,6 +844,7 @@ class Parser : public ParserBase<Parser> {
return nullptr;
}
V8_INLINE static FunctionLiteral* EmptyFunctionLiteral() { return nullptr; }
V8_INLINE static Block* NullBlock() { return nullptr; }
V8_INLINE static bool IsEmptyExpression(Expression* expr) {
return expr == nullptr;
......@@ -915,6 +922,11 @@ class Parser : public ParserBase<Parser> {
return new (zone()) ZoneList<Statement*>(size, zone());
}
V8_INLINE Block* NewBlock(ZoneList<const AstRawString*>* labels, int capacity,
bool ignore_completion_value, int pos) {
return factory()->NewBlock(labels, capacity, ignore_completion_value, pos);
}
V8_INLINE void AddParameterInitializationBlock(
const ParserFormalParameters& parameters, ZoneList<Statement*>* body,
bool is_async, bool* ok) {
......
......@@ -423,118 +423,12 @@ PreParser::Statement PreParser::ParseVariableStatement(
// VariableStatement ::
// VariableDeclarations ';'
Statement result = ParseVariableDeclarations(
var_context, nullptr, nullptr, nullptr, nullptr, nullptr, CHECK_OK);
Statement result =
ParseVariableDeclarations(var_context, nullptr, nullptr, CHECK_OK);
ExpectSemicolon(CHECK_OK);
return result;
}
// If the variable declaration declares exactly one non-const
// variable, then *var is set to that variable. In all other cases,
// *var is untouched; in particular, it is the caller's responsibility
// to initialize it properly. This mechanism is also used for the parsing
// of 'for-in' loops.
PreParser::Statement PreParser::ParseVariableDeclarations(
VariableDeclarationContext var_context, int* num_decl, bool* is_lexical,
bool* is_binding_pattern, Scanner::Location* first_initializer_loc,
Scanner::Location* bindings_loc, bool* ok) {
// VariableDeclarations ::
// ('var' | 'const') (Identifier ('=' AssignmentExpression)?)+[',']
//
// The ES6 Draft Rev3 specifies the following grammar for const declarations
//
// ConstDeclaration ::
// const ConstBinding (',' ConstBinding)* ';'
// ConstBinding ::
// Identifier '=' AssignmentExpression
//
// TODO(ES6):
// ConstBinding ::
// BindingPattern '=' AssignmentExpression
bool require_initializer = false;
bool lexical = false;
bool is_pattern = false;
if (peek() == Token::VAR) {
Consume(Token::VAR);
} else if (peek() == Token::CONST) {
// TODO(ES6): The ES6 Draft Rev4 section 12.2.2 reads:
//
// ConstDeclaration : const ConstBinding (',' ConstBinding)* ';'
//
// * It is a Syntax Error if the code that matches this production is not
// contained in extended code.
//
// However disallowing const in sloppy mode will break compatibility with
// existing pages. Therefore we keep allowing const with the old
// non-harmony semantics in sloppy mode.
Consume(Token::CONST);
DCHECK(var_context != kStatement);
require_initializer = true;
lexical = true;
} else if (peek() == Token::LET) {
Consume(Token::LET);
DCHECK(var_context != kStatement);
lexical = true;
} else {
*ok = false;
return Statement::Default();
}
// The scope of a var/const declared variable anywhere inside a function
// is the entire function (ECMA-262, 3rd, 10.1.3, and 12.2). The scope
// of a let declared variable is the scope of the immediately enclosing
// block.
int nvars = 0; // the number of variables declared
int bindings_start = peek_position();
do {
// Parse binding pattern.
if (nvars > 0) Consume(Token::COMMA);
int decl_pos = peek_position();
PreParserExpression pattern = PreParserExpression::Default();
{
ExpressionClassifier pattern_classifier(this);
pattern = ParsePrimaryExpression(CHECK_OK);
ValidateBindingPattern(CHECK_OK);
if (lexical) ValidateLetPattern(CHECK_OK);
}
is_pattern = pattern.IsObjectLiteral() || pattern.IsArrayLiteral();
Scanner::Location variable_loc = scanner()->location();
nvars++;
if (Check(Token::ASSIGN)) {
ExpressionClassifier classifier(this);
ParseAssignmentExpression(var_context != kForStatement, CHECK_OK);
ValidateExpression(CHECK_OK);
variable_loc.end_pos = scanner()->location().end_pos;
if (first_initializer_loc && !first_initializer_loc->IsValid()) {
*first_initializer_loc = variable_loc;
}
} else if ((require_initializer || is_pattern) &&
(var_context != kForStatement || !PeekInOrOf())) {
ReportMessageAt(
Scanner::Location(decl_pos, scanner()->location().end_pos),
MessageTemplate::kDeclarationMissingInitializer,
is_pattern ? "destructuring" : "const");
*ok = false;
return Statement::Default();
}
} while (peek() == Token::COMMA);
if (bindings_loc) {
*bindings_loc =
Scanner::Location(bindings_start, scanner()->location().end_pos);
}
if (num_decl != nullptr) *num_decl = nvars;
if (is_lexical != nullptr) *is_lexical = lexical;
if (is_binding_pattern != nullptr) *is_binding_pattern = is_pattern;
return Statement::Default();
}
PreParser::Statement PreParser::ParseFunctionDeclaration(bool* ok) {
Consume(Token::FUNCTION);
int pos = position();
......@@ -798,32 +692,35 @@ PreParser::Statement PreParser::ParseForStatement(bool* ok) {
ForEachStatement::VisitMode mode;
if (peek() == Token::VAR || peek() == Token::CONST ||
(peek() == Token::LET && IsNextLetKeyword())) {
int decl_count;
bool is_lexical;
bool is_binding_pattern;
Scanner::Location first_initializer_loc = Scanner::Location::invalid();
Scanner::Location bindings_loc = Scanner::Location::invalid();
ParseVariableDeclarations(kForStatement, &decl_count, &is_lexical,
&is_binding_pattern, &first_initializer_loc,
&bindings_loc, CHECK_OK);
if (is_lexical) has_lexical = true;
DeclarationParsingResult parsing_result;
ParseVariableDeclarations(kForStatement, &parsing_result, nullptr,
CHECK_OK);
if (parsing_result.descriptor.mode == CONST ||
parsing_result.descriptor.mode == LET) {
has_lexical = true;
}
if (CheckInOrOf(&mode)) {
if (decl_count != 1) {
ReportMessageAt(bindings_loc,
if (!*ok) return Statement::Default();
if (parsing_result.declarations.length() != 1) {
ReportMessageAt(parsing_result.bindings_loc,
MessageTemplate::kForInOfLoopMultiBindings,
ForEachStatement::VisitModeString(mode));
*ok = false;
return Statement::Default();
}
if (first_initializer_loc.IsValid() &&
bool is_binding_pattern =
parsing_result.declarations[0].pattern.IsObjectLiteral() ||
parsing_result.declarations[0].pattern.IsArrayLiteral();
if (parsing_result.first_initializer_loc.IsValid() &&
(is_strict(language_mode()) || mode == ForEachStatement::ITERATE ||
is_lexical || is_binding_pattern || allow_harmony_for_in())) {
has_lexical || is_binding_pattern || allow_harmony_for_in())) {
// Only increment the use count if we would have let this through
// without the flag.
if (use_counts_ != nullptr && allow_harmony_for_in()) {
++use_counts_[v8::Isolate::kForInInitializer];
}
ReportMessageAt(first_initializer_loc,
ReportMessageAt(parsing_result.first_initializer_loc,
MessageTemplate::kForInOfLoopInitializer,
ForEachStatement::VisitModeString(mode));
*ok = false;
......
......@@ -614,6 +614,7 @@ struct ParserTypes<PreParser> {
typedef PreParserIdentifier FormalParameter;
typedef PreParserFormalParameters FormalParameters;
typedef PreParserStatementList StatementList;
typedef PreParserStatement Block;
// For constructing objects returned by the traversing functions.
typedef PreParserFactory Factory;
......@@ -736,12 +737,6 @@ class PreParser : public ParserBase<PreParser> {
Statement ParseBlock(bool* ok);
Statement ParseVariableStatement(VariableDeclarationContext var_context,
bool* ok);
Statement ParseVariableDeclarations(VariableDeclarationContext var_context,
int* num_decl, bool* is_lexical,
bool* is_binding_pattern,
Scanner::Location* first_initializer_loc,
Scanner::Location* bindings_loc,
bool* ok);
Statement ParseExpressionOrLabelledStatement(
AllowLabelledFunctionStatement allow_function, bool* ok);
Statement ParseIfStatement(bool* ok);
......@@ -838,6 +833,12 @@ class PreParser : public ParserBase<PreParser> {
}
V8_INLINE void RewriteNonPattern(bool* ok) { ValidateExpression(ok); }
V8_INLINE void DeclareAndInitializeVariables(
PreParserStatement block,
const DeclarationDescriptor* declaration_descriptor,
const DeclarationParsingResult::Declaration* declaration,
ZoneList<const AstRawString*>* names, bool* ok) {}
V8_INLINE void QueueDestructuringAssignmentForRewriting(
PreParserExpression assignment) {}
V8_INLINE void QueueNonPatternForRewriting(PreParserExpression expr,
......@@ -917,6 +918,12 @@ class PreParser : public ParserBase<PreParser> {
UNREACHABLE();
}
V8_INLINE static void PushVariableName(FuncNameInferrer* fni,
PreParserIdentifier id) {
// PreParser should not use FuncNameInferrer.
UNREACHABLE();
}
V8_INLINE void PushPropertyName(FuncNameInferrer* fni,
PreParserExpression expression) {
// PreParser should not use FuncNameInferrer.
......@@ -1011,9 +1018,15 @@ class PreParser : public ParserBase<PreParser> {
V8_INLINE static PreParserExpressionList NullExpressionList() {
return PreParserExpressionList();
}
V8_INLINE static PreParserStatementList NullStatementList() {
return PreParserStatementList();
}
V8_INLINE static PreParserStatement NullBlock() {
return PreParserStatement::Default();
}
V8_INLINE PreParserIdentifier EmptyIdentifierString() const {
return PreParserIdentifier::Default();
}
......@@ -1023,6 +1036,10 @@ class PreParser : public ParserBase<PreParser> {
return PreParserExpression::Default();
}
V8_INLINE PreParserExpression GetLiteralUndefined(int position) {
return PreParserExpression::Default();
}
// Producing data during the recursive descent.
PreParserIdentifier GetSymbol() const;
......@@ -1084,6 +1101,12 @@ class PreParser : public ParserBase<PreParser> {
return PreParserStatementList();
}
V8_INLINE static PreParserStatement NewBlock(
ZoneList<const AstRawString*>* labels, int capacity,
bool ignore_completion_value, int pos) {
return PreParserStatement::Default();
}
V8_INLINE void AddParameterInitializationBlock(
const PreParserFormalParameters& parameters, PreParserStatementList body,
bool is_async, bool* ok) {}
......
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