Commit f3d5b13e authored by arv's avatar arv Committed by Commit bot

Classes: Implement correct name binding

Named class declarations and class expression have a const binding for
the name that is in TDZ for the extends expression.

BUG=v8:3330
LOG=Y
R=dslomov@chromium.org, adamk

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

Cr-Commit-Position: refs/heads/master@{#25360}
parent a210f368
......@@ -448,6 +448,9 @@ void AstNumberingVisitor::VisitClassLiteral(ClassLiteral* node) {
node->set_base_id(ReserveIdRange(ClassLiteral::num_ids()));
if (node->extends()) Visit(node->extends());
if (node->constructor()) Visit(node->constructor());
if (node->class_variable_proxy()) {
VisitVariableProxy(node->class_variable_proxy());
}
for (int i = 0; i < node->properties()->length(); i++) {
VisitObjectLiteralProperty(node->properties()->at(i));
}
......
......@@ -2641,6 +2641,8 @@ class ClassLiteral FINAL : public Expression {
Handle<String> name() const { return raw_name_->string(); }
const AstRawString* raw_name() const { return raw_name_; }
Scope* scope() const { return scope_; }
VariableProxy* class_variable_proxy() const { return class_variable_proxy_; }
Expression* extends() const { return extends_; }
Expression* constructor() const { return constructor_; }
ZoneList<Property*>* properties() const { return properties_; }
......@@ -2648,11 +2650,14 @@ class ClassLiteral FINAL : public Expression {
int end_position() const { return end_position_; }
protected:
ClassLiteral(Zone* zone, const AstRawString* name, Expression* extends,
ClassLiteral(Zone* zone, const AstRawString* name, Scope* scope,
VariableProxy* class_variable_proxy, Expression* extends,
Expression* constructor, ZoneList<Property*>* properties,
int start_position, int end_position)
: Expression(zone, start_position),
raw_name_(name),
scope_(scope),
class_variable_proxy_(class_variable_proxy),
extends_(extends),
constructor_(constructor),
properties_(properties),
......@@ -2660,6 +2665,8 @@ class ClassLiteral FINAL : public Expression {
private:
const AstRawString* raw_name_;
Scope* scope_;
VariableProxy* class_variable_proxy_;
Expression* extends_;
Expression* constructor_;
ZoneList<Property*>* properties_;
......@@ -3500,12 +3507,14 @@ class AstNodeFactory FINAL BASE_EMBEDDED {
position);
}
ClassLiteral* NewClassLiteral(const AstRawString* name, Expression* extends,
ClassLiteral* NewClassLiteral(const AstRawString* name, Scope* scope,
VariableProxy* proxy, Expression* extends,
Expression* constructor,
ZoneList<ObjectLiteral::Property*>* properties,
int start_position, int end_position) {
return new (zone_) ClassLiteral(zone_, name, extends, constructor,
properties, start_position, end_position);
return new (zone_)
ClassLiteral(zone_, name, scope, proxy, extends, constructor,
properties, start_position, end_position);
}
NativeFunctionLiteral* NewNativeFunctionLiteral(const AstRawString* name,
......
......@@ -1073,41 +1073,12 @@ void FullCodeGenerator::VisitBlock(Block* stmt) {
NestedBlock nested_block(this, stmt);
SetStatementPosition(stmt);
Scope* saved_scope = scope();
// Push a block context when entering a block with block scoped variables.
if (stmt->scope() == NULL) {
PrepareForBailoutForId(stmt->EntryId(), NO_REGISTERS);
} else {
scope_ = stmt->scope();
DCHECK(!scope_->is_module_scope());
{ Comment cmnt(masm_, "[ Extend block context");
__ Push(scope_->GetScopeInfo());
PushFunctionArgumentForContextAllocation();
__ CallRuntime(Runtime::kPushBlockContext, 2);
// Replace the context stored in the frame.
StoreToFrameField(StandardFrameConstants::kContextOffset,
context_register());
PrepareForBailoutForId(stmt->EntryId(), NO_REGISTERS);
}
{ Comment cmnt(masm_, "[ Declarations");
VisitDeclarations(scope_->declarations());
PrepareForBailoutForId(stmt->DeclsId(), NO_REGISTERS);
}
}
VisitStatements(stmt->statements());
scope_ = saved_scope;
__ bind(nested_block.break_label());
// Pop block context if necessary.
if (stmt->scope() != NULL) {
LoadContextField(context_register(), Context::PREVIOUS_INDEX);
// Update local stack frame context field.
StoreToFrameField(StandardFrameConstants::kContextOffset,
context_register());
{
EnterBlockScopeIfNeeded block_scope_state(
this, stmt->scope(), stmt->EntryId(), stmt->DeclsId(), stmt->ExitId());
VisitStatements(stmt->statements());
__ bind(nested_block.break_label());
}
PrepareForBailoutForId(stmt->ExitId(), NO_REGISTERS);
}
......@@ -1608,26 +1579,38 @@ void FullCodeGenerator::VisitFunctionLiteral(FunctionLiteral* expr) {
void FullCodeGenerator::VisitClassLiteral(ClassLiteral* lit) {
Comment cmnt(masm_, "[ ClassLiteral");
if (lit->raw_name() != NULL) {
__ Push(lit->name());
} else {
__ Push(isolate()->factory()->undefined_value());
}
{
EnterBlockScopeIfNeeded block_scope_state(
this, lit->scope(), BailoutId::None(), BailoutId::None(),
BailoutId::None());
if (lit->extends() != NULL) {
VisitForStackValue(lit->extends());
} else {
__ Push(isolate()->factory()->the_hole_value());
}
if (lit->raw_name() != NULL) {
__ Push(lit->name());
} else {
__ Push(isolate()->factory()->undefined_value());
}
VisitForStackValue(lit->constructor());
if (lit->extends() != NULL) {
VisitForStackValue(lit->extends());
} else {
__ Push(isolate()->factory()->the_hole_value());
}
VisitForStackValue(lit->constructor());
__ Push(script());
__ Push(Smi::FromInt(lit->start_position()));
__ Push(Smi::FromInt(lit->end_position()));
__ Push(script());
__ Push(Smi::FromInt(lit->start_position()));
__ Push(Smi::FromInt(lit->end_position()));
__ CallRuntime(Runtime::kDefineClass, 6);
EmitClassDefineProperties(lit);
__ CallRuntime(Runtime::kDefineClass, 6);
EmitClassDefineProperties(lit);
if (lit->scope() != NULL) {
DCHECK_NOT_NULL(lit->class_variable_proxy());
EmitVariableAssignment(lit->class_variable_proxy()->var(),
Token::INIT_CONST);
}
}
context()->Plug(result_register());
}
......@@ -1795,6 +1778,49 @@ bool BackEdgeTable::Verify(Isolate* isolate, Code* unoptimized) {
#endif // DEBUG
FullCodeGenerator::EnterBlockScopeIfNeeded::EnterBlockScopeIfNeeded(
FullCodeGenerator* codegen, Scope* scope, BailoutId entry_id,
BailoutId declarations_id, BailoutId exit_id)
: codegen_(codegen), scope_(scope), exit_id_(exit_id) {
saved_scope_ = codegen_->scope();
if (scope == NULL) {
codegen_->PrepareForBailoutForId(entry_id, NO_REGISTERS);
} else {
codegen_->scope_ = scope;
{
Comment cmnt(masm(), "[ Extend block context");
__ Push(scope->GetScopeInfo());
codegen_->PushFunctionArgumentForContextAllocation();
__ CallRuntime(Runtime::kPushBlockContext, 2);
// Replace the context stored in the frame.
codegen_->StoreToFrameField(StandardFrameConstants::kContextOffset,
codegen_->context_register());
codegen_->PrepareForBailoutForId(entry_id, NO_REGISTERS);
}
{
Comment cmnt(masm(), "[ Declarations");
codegen_->VisitDeclarations(scope->declarations());
codegen_->PrepareForBailoutForId(declarations_id, NO_REGISTERS);
}
}
}
FullCodeGenerator::EnterBlockScopeIfNeeded::~EnterBlockScopeIfNeeded() {
if (scope_ != NULL) {
codegen_->LoadContextField(codegen_->context_register(),
Context::PREVIOUS_INDEX);
// Update local stack frame context field.
codegen_->StoreToFrameField(StandardFrameConstants::kContextOffset,
codegen_->context_register());
}
codegen_->PrepareForBailoutForId(exit_id_, NO_REGISTERS);
codegen_->scope_ = saved_scope_;
}
#undef __
......
......@@ -882,6 +882,22 @@ class FullCodeGenerator: public AstVisitor {
virtual bool IsEffect() const { return true; }
};
class EnterBlockScopeIfNeeded {
public:
EnterBlockScopeIfNeeded(FullCodeGenerator* codegen, Scope* scope,
BailoutId entry_id, BailoutId declarations_id,
BailoutId exit_id);
~EnterBlockScopeIfNeeded();
private:
MacroAssembler* masm() const { return codegen_->masm(); }
FullCodeGenerator* codegen_;
Scope* scope_;
Scope* saved_scope_;
BailoutId exit_id_;
};
MacroAssembler* masm_;
CompilationInfo* info_;
Scope* scope_;
......
......@@ -685,13 +685,6 @@ Expression* ParserTraits::SuperReference(Scope* scope, AstNodeFactory* factory,
pos);
}
Expression* ParserTraits::ClassExpression(
const AstRawString* name, Expression* extends, Expression* constructor,
ZoneList<ObjectLiteral::Property*>* properties, int start_position,
int end_position, AstNodeFactory* factory) {
return factory->NewClassLiteral(name, extends, constructor, properties,
start_position, end_position);
}
Expression* ParserTraits::DefaultConstructor(bool call_super, Scope* scope,
int pos, int end_pos) {
......@@ -777,6 +770,14 @@ FunctionLiteral* ParserTraits::ParseFunctionLiteral(
}
ClassLiteral* ParserTraits::ParseClassLiteral(
const AstRawString* name, Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos, bool* ok) {
return parser_->ParseClassLiteral(name, class_name_location,
name_is_strict_reserved, pos, ok);
}
Parser::Parser(CompilationInfo* info, ParseInfo* parse_info)
: ParserBase<ParserTraits>(&scanner_, parse_info->stack_limit,
info->extension(), NULL, info->zone(), this),
......@@ -1993,8 +1994,8 @@ Statement* Parser::ParseClassDeclaration(ZoneList<const AstRawString*>* names,
bool is_strict_reserved = false;
const AstRawString* name =
ParseIdentifierOrStrictReservedWord(&is_strict_reserved, CHECK_OK);
Expression* value = ParseClassLiteral(name, scanner()->location(),
is_strict_reserved, pos, CHECK_OK);
ClassLiteral* value = ParseClassLiteral(name, scanner()->location(),
is_strict_reserved, pos, CHECK_OK);
VariableProxy* proxy = NewUnresolved(name, LET, Interface::NewValue());
Declaration* declaration =
......@@ -3893,6 +3894,90 @@ PreParser::PreParseResult Parser::ParseLazyFunctionBodyWithPreParser(
}
ClassLiteral* Parser::ParseClassLiteral(const AstRawString* name,
Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos,
bool* ok) {
// All parts of a ClassDeclaration and ClassExpression are strict code.
if (name_is_strict_reserved) {
ReportMessageAt(class_name_location, "unexpected_strict_reserved");
*ok = false;
return NULL;
}
if (IsEvalOrArguments(name)) {
ReportMessageAt(class_name_location, "strict_eval_arguments");
*ok = false;
return NULL;
}
Scope* block_scope = NewScope(scope_, BLOCK_SCOPE);
BlockState block_state(&scope_, block_scope);
scope_->SetStrictMode(STRICT);
scope_->SetScopeName(name);
VariableProxy* proxy = NULL;
if (name != NULL) {
proxy = NewUnresolved(name, CONST, Interface::NewConst());
Declaration* declaration =
factory()->NewVariableDeclaration(proxy, CONST, block_scope, pos);
Declare(declaration, true, CHECK_OK);
}
Expression* extends = NULL;
if (Check(Token::EXTENDS)) {
block_scope->set_start_position(scanner()->location().end_pos);
extends = ParseLeftHandSideExpression(CHECK_OK);
} else {
block_scope->set_start_position(scanner()->location().end_pos);
}
ZoneList<ObjectLiteral::Property*>* properties = NewPropertyList(4, zone());
Expression* constructor = NULL;
bool has_seen_constructor = false;
Expect(Token::LBRACE, CHECK_OK);
while (peek() != Token::RBRACE) {
if (Check(Token::SEMICOLON)) continue;
if (fni_ != NULL) fni_->Enter();
const bool in_class = true;
const bool is_static = false;
ObjectLiteral::Property* property = ParsePropertyDefinition(
NULL, in_class, is_static, &has_seen_constructor, CHECK_OK);
if (has_seen_constructor && constructor == NULL) {
constructor = GetPropertyValue(property);
} else {
properties->Add(property, zone());
}
if (fni_ != NULL) {
fni_->Infer();
fni_->Leave();
}
}
Expect(Token::RBRACE, CHECK_OK);
int end_pos = scanner()->location().end_pos;
if (constructor == NULL) {
constructor =
DefaultConstructor(extends != NULL, block_scope, pos, end_pos);
}
block_scope->set_end_position(end_pos);
block_scope = block_scope->FinalizeBlockScope();
if (name != NULL) {
DCHECK_NOT_NULL(proxy);
DCHECK_NOT_NULL(block_scope);
proxy->var()->set_initializer_position(end_pos);
}
return factory()->NewClassLiteral(name, block_scope, proxy, extends,
constructor, properties, pos, end_pos);
}
Expression* Parser::ParseV8Intrinsic(bool* ok) {
// CallRuntime ::
// '%' Identifier Arguments
......
......@@ -538,12 +538,6 @@ class ParserTraits {
int pos = RelocInfo::kNoPosition);
Expression* SuperReference(Scope* scope, AstNodeFactory* factory,
int pos = RelocInfo::kNoPosition);
Expression* ClassExpression(const AstRawString* name, Expression* extends,
Expression* constructor,
ZoneList<ObjectLiteral::Property*>* properties,
int start_position, int end_position,
AstNodeFactory* factory);
Expression* DefaultConstructor(bool call_super, Scope* scope, int pos,
int end_pos);
Literal* ExpressionFromLiteral(Token::Value token, int pos, Scanner* scanner,
......@@ -583,6 +577,12 @@ class ParserTraits {
V8_INLINE ZoneList<Statement*>* ParseEagerFunctionBody(
const AstRawString* name, int pos, Variable* fvar,
Token::Value fvar_init_op, bool is_generator, bool* ok);
ClassLiteral* ParseClassLiteral(const AstRawString* name,
Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos,
bool* ok);
V8_INLINE void CheckConflictingVarDeclarations(v8::internal::Scope* scope,
bool* ok);
......@@ -763,6 +763,12 @@ class Parser : public ParserBase<ParserTraits> {
int function_token_position, FunctionLiteral::FunctionType type,
FunctionLiteral::ArityRestriction arity_restriction, bool* ok);
ClassLiteral* ParseClassLiteral(const AstRawString* name,
Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos,
bool* ok);
// Magical syntax support.
Expression* ParseV8Intrinsic(bool* ok);
......
......@@ -132,6 +132,14 @@ PreParser::PreParseResult PreParser::PreParseLazyFunction(
}
PreParserExpression PreParserTraits::ParseClassLiteral(
PreParserIdentifier name, Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos, bool* ok) {
return pre_parser_->ParseClassLiteral(name, class_name_location,
name_is_strict_reserved, pos, ok);
}
// Preparsing checks a JavaScript program and emits preparse-data that helps
// a later parsing to be faster.
// See preparser-data.h for the data.
......@@ -928,6 +936,47 @@ void PreParser::ParseLazyFunctionLiteralBody(bool* ok) {
}
PreParserExpression PreParser::ParseClassLiteral(
PreParserIdentifier name, Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos, bool* ok) {
// All parts of a ClassDeclaration and ClassExpression are strict code.
if (name_is_strict_reserved) {
ReportMessageAt(class_name_location, "unexpected_strict_reserved");
*ok = false;
return EmptyExpression();
}
if (IsEvalOrArguments(name)) {
ReportMessageAt(class_name_location, "strict_eval_arguments");
*ok = false;
return EmptyExpression();
}
PreParserScope scope = NewScope(scope_, BLOCK_SCOPE);
BlockState block_state(&scope_, &scope);
scope_->SetStrictMode(STRICT);
scope_->SetScopeName(name);
if (Check(Token::EXTENDS)) {
ParseLeftHandSideExpression(CHECK_OK);
}
bool has_seen_constructor = false;
Expect(Token::LBRACE, CHECK_OK);
while (peek() != Token::RBRACE) {
if (Check(Token::SEMICOLON)) continue;
const bool in_class = true;
const bool is_static = false;
ParsePropertyDefinition(NULL, in_class, is_static, &has_seen_constructor,
CHECK_OK);
}
Expect(Token::RBRACE, CHECK_OK);
return Expression::Default();
}
PreParser::Expression PreParser::ParseV8Intrinsic(bool* ok) {
// CallRuntime ::
// '%' Identifier Arguments
......
......@@ -490,10 +490,6 @@ class ParserBase : public Traits {
bool* ok);
ExpressionT ParseArrowFunctionLiteral(int start_pos, ExpressionT params_ast,
bool* ok);
ExpressionT ParseClassLiteral(IdentifierT name,
Scanner::Location function_name_location,
bool name_is_strict_reserved, int pos,
bool* ok);
// Checks if the expression is a valid reference expression (e.g., on the
// left-hand side of assignments). Although ruled out by ECMA as early errors,
......@@ -1090,12 +1086,13 @@ class PreParserFactory {
int position) {
return PreParserExpression::Default();
}
PreParserExpression NewClassLiteral(PreParserIdentifier name,
PreParserExpression extends,
PreParserExpression constructor,
PreParserExpressionList properties,
int start_position, int end_position) {
return PreParserExpression::Default();
// Return the object itself as AstVisitor and implement the needed
// dummy method right in this class.
PreParserFactory* visitor() { return this; }
int* ast_properties() {
static int dummy = 42;
return &dummy;
}
};
......@@ -1308,13 +1305,6 @@ class PreParserTraits {
return PreParserExpression::Super();
}
static PreParserExpression ClassExpression(
PreParserIdentifier name, PreParserExpression extends,
PreParserExpression constructor, PreParserExpressionList properties,
int start_position, int end_position, PreParserFactory* factory) {
return PreParserExpression::Default();
}
static PreParserExpression DefaultConstructor(bool call_super,
PreParserScope* scope, int pos,
int end_pos) {
......@@ -1387,6 +1377,11 @@ class PreParserTraits {
int function_token_position, FunctionLiteral::FunctionType type,
FunctionLiteral::ArityRestriction arity_restriction, bool* ok);
PreParserExpression ParseClassLiteral(PreParserIdentifier name,
Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos,
bool* ok);
private:
PreParser* pre_parser_;
};
......@@ -1524,6 +1519,11 @@ class PreParser : public ParserBase<PreParserTraits> {
FunctionLiteral::ArityRestriction arity_restriction, bool* ok);
void ParseLazyFunctionLiteralBody(bool* ok);
PreParserExpression ParseClassLiteral(PreParserIdentifier name,
Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos,
bool* ok);
bool CheckInOrOf(bool accept_OF);
};
......@@ -2722,78 +2722,6 @@ typename ParserBase<Traits>::ExpressionT ParserBase<
}
template <class Traits>
typename ParserBase<Traits>::ExpressionT ParserBase<Traits>::ParseClassLiteral(
IdentifierT name, Scanner::Location class_name_location,
bool name_is_strict_reserved, int pos, bool* ok) {
// All parts of a ClassDeclaration or a ClassExpression are strict code.
if (name_is_strict_reserved) {
ReportMessageAt(class_name_location, "unexpected_strict_reserved");
*ok = false;
return this->EmptyExpression();
}
if (this->IsEvalOrArguments(name)) {
ReportMessageAt(class_name_location, "strict_eval_arguments");
*ok = false;
return this->EmptyExpression();
}
bool has_extends = false;
ExpressionT extends = this->EmptyExpression();
if (Check(Token::EXTENDS)) {
typename Traits::Type::ScopePtr scope = this->NewScope(scope_, BLOCK_SCOPE);
BlockState block_state(&scope_, Traits::Type::ptr_to_scope(scope));
scope_->SetStrictMode(STRICT);
extends = this->ParseLeftHandSideExpression(CHECK_OK);
has_extends = true;
}
// TODO(arv): Implement scopes and name binding in class body only.
typename Traits::Type::ScopePtr scope = this->NewScope(scope_, BLOCK_SCOPE);
BlockState block_state(&scope_, Traits::Type::ptr_to_scope(scope));
scope_->SetStrictMode(STRICT);
scope_->SetScopeName(name);
typename Traits::Type::PropertyList properties =
this->NewPropertyList(4, zone_);
ExpressionT constructor = this->EmptyExpression();
bool has_seen_constructor = false;
Expect(Token::LBRACE, CHECK_OK);
while (peek() != Token::RBRACE) {
if (Check(Token::SEMICOLON)) continue;
if (fni_ != NULL) fni_->Enter();
const bool in_class = true;
const bool is_static = false;
bool old_has_seen_constructor = has_seen_constructor;
ObjectLiteralPropertyT property = this->ParsePropertyDefinition(
NULL, in_class, is_static, &has_seen_constructor, CHECK_OK);
if (has_seen_constructor != old_has_seen_constructor) {
constructor = this->GetPropertyValue(property);
} else {
properties->Add(property, zone());
}
if (fni_ != NULL) {
fni_->Infer();
fni_->Leave();
}
}
int end_pos = peek_position();
Expect(Token::RBRACE, CHECK_OK);
if (!has_seen_constructor) {
constructor =
this->DefaultConstructor(has_extends, scope_, pos, end_pos + 1);
}
return this->ClassExpression(name, extends, constructor, properties, pos,
end_pos + 1, factory());
}
template <typename Traits>
typename ParserBase<Traits>::ExpressionT
ParserBase<Traits>::CheckAndRewriteReferenceExpression(
......
......@@ -673,15 +673,98 @@ function assertAccessorDescriptor(object, name) {
})();
/* TODO(arv): Implement
(function TestNameBindingInConstructor() {
(function TestNameBindingConst() {
assertThrows('class C { constructor() { C = 42; } }', SyntaxError);
assertThrows('(class C { constructor() { C = 42; } })', SyntaxError);
assertThrows('class C { m() { C = 42; } }', SyntaxError);
assertThrows('(class C { m() { C = 42; } })', SyntaxError);
assertThrows('class C { get x() { C = 42; } }', SyntaxError);
assertThrows('(class C { get x() { C = 42; } })', SyntaxError);
assertThrows('class C { set x(_) { C = 42; } }', SyntaxError);
assertThrows('(class C { set x(_) { C = 42; } })', SyntaxError);
})();
(function TestNameBinding() {
var C2;
class C {
constructor() {
assertThrows(function() {
C = 42;
}, ReferenceError);
C2 = C;
}
m() {
C2 = C;
}
get x() {
C2 = C;
}
set x(_) {
C2 = C;
}
}
new C();
assertEquals(C, C2);
C2 = undefined;
new C().m();
assertEquals(C, C2);
C2 = undefined;
new C().x;
assertEquals(C, C2);
C2 = undefined;
new C().x = 1;
assertEquals(C, C2);
})();
(function TestNameBindingExpression() {
var C3;
var C = class C2 {
constructor() {
assertEquals(C2, C);
C3 = C2;
}
m() {
assertEquals(C2, C);
C3 = C2;
}
get x() {
assertEquals(C2, C);
C3 = C2;
}
set x(_) {
assertEquals(C2, C);
C3 = C2;
}
}
new C();
assertEquals(C, C3);
C3 = undefined;
new C().m();
assertEquals(C, C3);
C3 = undefined;
new C().x;
assertEquals(C, C3);
C3 = undefined;
new C().x = 1;
assertEquals(C, C3);
})();
(function TestNameBindingInExtendsExpression() {
assertThrows(function() {
class x extends x {}
}, ReferenceError);
assertThrows(function() {
(class x extends x {});
}, ReferenceError);
assertThrows(function() {
var x = (class x extends x {});
}, ReferenceError);
})();
*/
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