Commit 8ae19e08 authored by Caitlin Potter's avatar Caitlin Potter Committed by Commit Bot

[esnext] re-implement template strings

- Add a new bytecode for the ToString operation, replacing the old
intrinsic call (currently does not collect type feedback).
- Add a new AST node to represent TemplateLiterals, and avoid
generating unnecessary ToString operations in some simple cases.
- Use a single feedback slot for each string addition, because the
type feedback should always be the same for each addition

This seems to produce a very slight improvement on JSTests benchmarks
and bench-ruben.js from v8:7415, and it's possible that type feedback
for the ToString bytecode could provide more opportunities to eliminate
the runtime call in TurboFan.

Doesn't touch tagged templates

BUG=v8:7415
R=rmcilroy@chromium.org, ishell@chromium.org, bmeurer@chromium.org

Change-Id: If5a8c68558431f058db894d65776324abf54218e
Reviewed-on: https://chromium-review.googlesource.com/945408Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarRoss McIlroy <rmcilroy@chromium.org>
Commit-Queue: Caitlin Potter <caitp@igalia.com>
Cr-Commit-Position: refs/heads/master@{#51853}
parent 198148ba
......@@ -535,6 +535,15 @@ void AstTraversalVisitor<Subclass>::VisitGetTemplateObject(
PROCESS_EXPRESSION(expr);
}
template <class Subclass>
void AstTraversalVisitor<Subclass>::VisitTemplateLiteral(
TemplateLiteral* expr) {
PROCESS_EXPRESSION(expr);
for (Expression* sub : *expr->substitutions()) {
RECURSE_EXPRESSION(Visit(sub));
}
}
template <class Subclass>
void AstTraversalVisitor<Subclass>::VisitImportCallExpression(
ImportCallExpression* expr) {
......
......@@ -99,6 +99,7 @@ namespace internal {
V(Spread) \
V(SuperCallReference) \
V(SuperPropertyReference) \
V(TemplateLiteral) \
V(ThisFunction) \
V(Throw) \
V(UnaryOperation) \
......@@ -2653,6 +2654,26 @@ class GetTemplateObject final : public Expression {
const ZoneList<const AstRawString*>* raw_strings_;
};
class TemplateLiteral final : public Expression {
public:
using StringList = ZoneList<const AstRawString*>;
using ExpressionList = ZoneList<Expression*>;
const StringList* string_parts() const { return string_parts_; }
const ExpressionList* substitutions() const { return substitutions_; }
private:
friend class AstNodeFactory;
TemplateLiteral(const StringList* parts, const ExpressionList* substitutions,
int pos)
: Expression(pos, kTemplateLiteral),
string_parts_(parts),
substitutions_(substitutions) {}
const StringList* string_parts_;
const ExpressionList* substitutions_;
};
// ----------------------------------------------------------------------------
// Basic visitor
// Sub-class should parametrize AstVisitor with itself, e.g.:
......@@ -3229,6 +3250,12 @@ class AstNodeFactory final BASE_EMBEDDED {
return new (zone_) GetTemplateObject(cooked_strings, raw_strings, pos);
}
TemplateLiteral* NewTemplateLiteral(
const ZoneList<const AstRawString*>* string_parts,
const ZoneList<Expression*>* substitutions, int pos) {
return new (zone_) TemplateLiteral(string_parts, substitutions, pos);
}
ImportCallExpression* NewImportCallExpression(Expression* args, int pos) {
return new (zone_) ImportCallExpression(args, pos);
}
......
......@@ -466,6 +466,26 @@ void CallPrinter::VisitGetIterator(GetIterator* node) {
void CallPrinter::VisitGetTemplateObject(GetTemplateObject* node) {}
void CallPrinter::VisitTemplateLiteral(TemplateLiteral* node) {
if (node->substitutions()->length() && node->position() < position_ &&
node->substitutions()->last()->position() <= position_) {
found_ = true;
Print("`...${");
for (Expression* sub : *node->substitutions()) {
if (sub->position() < position_) continue;
Find(sub, true);
}
Print("}...`");
done_ = true;
} else if (node->string_parts()->length() == 1) {
Print("`");
PrintLiteral(node->string_parts()->first(), false);
Print("`");
} else {
Print("`...`");
}
}
void CallPrinter::VisitImportCallExpression(ImportCallExpression* node) {
Print("ImportCall(");
Find(node->argument(), true);
......@@ -1332,6 +1352,17 @@ void AstPrinter::VisitGetTemplateObject(GetTemplateObject* node) {
IndentedScope indent(this, "GET-TEMPLATE-OBJECT", node->position());
}
void AstPrinter::VisitTemplateLiteral(TemplateLiteral* node) {
IndentedScope indent(this, "TEMPLATE-LITERAL", node->position());
const AstRawString* string = node->string_parts()->first();
if (!string->IsEmpty()) PrintLiteralIndented("SPAN", string, true);
for (int i = 0; i < node->string_parts()->length();) {
PrintIndentedVisit("EXPR", node->substitutions()->at(i++));
string = node->string_parts()->at(i);
if (!string->IsEmpty()) PrintLiteralIndented("SPAN", string, true);
}
}
void AstPrinter::VisitImportCallExpression(ImportCallExpression* node) {
IndentedScope indent(this, "IMPORT-CALL", node->position());
Visit(node->argument());
......
......@@ -2528,6 +2528,12 @@ void BytecodeGraphBuilder::VisitToObject() {
BuildCastOperator(javascript()->ToObject());
}
void BytecodeGraphBuilder::VisitToString() {
Node* value =
NewNode(javascript()->ToString(), environment()->LookupAccumulator());
environment()->BindAccumulator(value, Environment::kAttachFrameState);
}
void BytecodeGraphBuilder::VisitToNumber() {
PrepareEagerCheckpoint();
Node* object = environment()->LookupAccumulator();
......
......@@ -507,6 +507,7 @@ bool BytecodeHasNoSideEffect(interpreter::Bytecode bytecode) {
case Bytecode::kToObject:
case Bytecode::kToNumber:
case Bytecode::kToName:
case Bytecode::kToString:
// Misc.
case Bytecode::kForInEnumerate:
case Bytecode::kForInPrepare:
......
......@@ -1004,6 +1004,11 @@ BytecodeArrayBuilder& BytecodeArrayBuilder::ToName(Register out) {
return *this;
}
BytecodeArrayBuilder& BytecodeArrayBuilder::ToString() {
OutputToString();
return *this;
}
BytecodeArrayBuilder& BytecodeArrayBuilder::ToNumber(int feedback_slot) {
OutputToNumber(feedback_slot);
return *this;
......
......@@ -376,6 +376,7 @@ class V8_EXPORT_PRIVATE BytecodeArrayBuilder final {
// Converts accumulator and stores result in register |out|.
BytecodeArrayBuilder& ToObject(Register out);
BytecodeArrayBuilder& ToName(Register out);
BytecodeArrayBuilder& ToString();
// Converts accumulator and stores result back in accumulator.
BytecodeArrayBuilder& ToNumber(int feedback_slot);
......
......@@ -593,6 +593,11 @@ class BytecodeGenerator::ExpressionResultScope {
type_hint_ = TypeHint::kBoolean;
}
void SetResultIsString() {
DCHECK_EQ(type_hint_, TypeHint::kAny);
type_hint_ = TypeHint::kString;
}
TypeHint type_hint() const { return type_hint_; }
private:
......@@ -2057,6 +2062,7 @@ void BytecodeGenerator::VisitLiteral(Literal* expr) {
break;
case Literal::kString:
builder()->LoadLiteral(expr->AsRawString());
execution_result()->SetResultIsString();
break;
case Literal::kSymbol:
builder()->LoadLiteral(expr->AsSymbol());
......@@ -3982,13 +3988,23 @@ void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* expr) {
Expression* subexpr;
Smi* literal;
if (expr->IsSmiLiteralOperation(&subexpr, &literal)) {
VisitForAccumulatorValue(subexpr);
TypeHint type_hint = VisitForAccumulatorValue(subexpr);
builder()->SetExpressionPosition(expr);
builder()->BinaryOperationSmiLiteral(expr->op(), literal,
feedback_index(slot));
if (expr->op() == Token::ADD && type_hint == TypeHint::kString) {
execution_result()->SetResultIsString();
}
} else {
Register lhs = VisitForRegisterValue(expr->left());
VisitForAccumulatorValue(expr->right());
TypeHint lhs_type = VisitForAccumulatorValue(expr->left());
Register lhs = register_allocator()->NewRegister();
builder()->StoreAccumulatorInRegister(lhs);
TypeHint rhs_type = VisitForAccumulatorValue(expr->right());
if (expr->op() == Token::ADD &&
(lhs_type == TypeHint::kString || rhs_type == TypeHint::kString)) {
execution_result()->SetResultIsString();
}
builder()->SetExpressionPosition(expr);
builder()->BinaryOperation(expr->op(), lhs, feedback_index(slot));
}
......@@ -3996,7 +4012,7 @@ void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* expr) {
void BytecodeGenerator::VisitNaryArithmeticExpression(NaryOperation* expr) {
// TODO(leszeks): Add support for lhs smi in commutative ops.
VisitForAccumulatorValue(expr->first());
TypeHint type_hint = VisitForAccumulatorValue(expr->first());
for (size_t i = 0; i < expr->subsequent_length(); ++i) {
RegisterAllocationScope register_scope(this);
......@@ -4008,13 +4024,19 @@ void BytecodeGenerator::VisitNaryArithmeticExpression(NaryOperation* expr) {
} else {
Register lhs = register_allocator()->NewRegister();
builder()->StoreAccumulatorInRegister(lhs);
VisitForAccumulatorValue(expr->subsequent(i));
TypeHint rhs_hint = VisitForAccumulatorValue(expr->subsequent(i));
if (rhs_hint == TypeHint::kString) type_hint = TypeHint::kString;
builder()->SetExpressionPosition(expr->subsequent_op_position(i));
builder()->BinaryOperation(
expr->op(), lhs,
feedback_index(feedback_spec()->AddBinaryOpICSlot()));
}
}
if (type_hint == TypeHint::kString && expr->op() == Token::ADD) {
// If any operand of an ADD is a String, a String is produced.
execution_result()->SetResultIsString();
}
}
void BytecodeGenerator::VisitSpread(Spread* expr) { Visit(expr->expression()); }
......@@ -4195,6 +4217,53 @@ void BytecodeGenerator::VisitGetTemplateObject(GetTemplateObject* expr) {
builder()->GetTemplateObject(entry, feedback_index(literal_slot));
}
void BytecodeGenerator::VisitTemplateLiteral(TemplateLiteral* expr) {
const TemplateLiteral::StringList& parts = *expr->string_parts();
const TemplateLiteral::ExpressionList& substitutions = *expr->substitutions();
// Template strings with no substitutions are turned into StringLiterals.
DCHECK_GT(substitutions.length(), 0);
DCHECK_EQ(parts.length(), substitutions.length() + 1);
// Generate string concatenation
// TODO(caitp): Don't generate feedback slot if it's not used --- introduce
// a simple, concise, reusable mechanism to lazily create reusable slots.
FeedbackSlot slot = feedback_spec()->AddBinaryOpICSlot();
Register last_part = register_allocator()->NewRegister();
bool last_part_valid = false;
builder()->SetExpressionPosition(expr);
for (int i = 0; i < substitutions.length(); ++i) {
if (i != 0) {
builder()->StoreAccumulatorInRegister(last_part);
last_part_valid = true;
}
if (!parts[i]->IsEmpty()) {
builder()->LoadLiteral(parts[i]);
if (last_part_valid) {
builder()->BinaryOperation(Token::ADD, last_part, feedback_index(slot));
}
builder()->StoreAccumulatorInRegister(last_part);
last_part_valid = true;
}
TypeHint type_hint = VisitForAccumulatorValue(substitutions[i]);
if (type_hint != TypeHint::kString) {
builder()->ToString();
}
if (last_part_valid) {
builder()->BinaryOperation(Token::ADD, last_part, feedback_index(slot));
}
last_part_valid = false;
}
if (!parts.last()->IsEmpty()) {
builder()->StoreAccumulatorInRegister(last_part);
builder()->LoadLiteral(parts.last());
builder()->BinaryOperation(Token::ADD, last_part, feedback_index(slot));
}
}
void BytecodeGenerator::VisitThisFunction(ThisFunction* expr) {
builder()->LoadAccumulatorWithRegister(Register::function_closure());
}
......
......@@ -66,7 +66,7 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
using ToBooleanMode = BytecodeArrayBuilder::ToBooleanMode;
enum class TestFallthrough { kThen, kElse, kNone };
enum class TypeHint { kAny, kBoolean };
enum class TypeHint { kAny, kBoolean, kString };
void GenerateBytecodeBody();
void AllocateDeferredConstants(Isolate* isolate, Handle<Script> script);
......
......@@ -223,6 +223,7 @@ namespace interpreter {
V(ToNumber, AccumulatorUse::kReadWrite, OperandType::kIdx) \
V(ToNumeric, AccumulatorUse::kReadWrite, OperandType::kIdx) \
V(ToObject, AccumulatorUse::kRead, OperandType::kRegOut) \
V(ToString, AccumulatorUse::kReadWrite) \
\
/* Literals */ \
V(CreateRegExpLiteral, AccumulatorUse::kWrite, OperandType::kIdx, \
......
......@@ -1304,6 +1304,14 @@ IGNITION_HANDLER(ToObject, InterpreterAssembler) {
Dispatch();
}
// ToString
//
// Convert the accumulator to a String.
IGNITION_HANDLER(ToString, InterpreterAssembler) {
SetAccumulator(ToString_Inline(GetContext(), GetAccumulator()));
Dispatch();
}
class IncDecAssembler : public UnaryNumericOpAssembler {
public:
explicit IncDecAssembler(CodeAssemblerState* state, Bytecode bytecode,
......
......@@ -3492,52 +3492,10 @@ Expression* Parser::CloseTemplateLiteral(TemplateLiteralState* state, int start,
DCHECK_EQ(cooked_strings->length(), expressions->length() + 1);
if (!tag) {
const AstRawString* first_string = cooked_strings->at(0);
if (expressions->length() == 0) {
return factory()->NewStringLiteral(first_string, kNoSourcePosition);
if (cooked_strings->length() == 1) {
return factory()->NewStringLiteral(cooked_strings->first(), pos);
}
size_t num_empty =
std::count_if(cooked_strings->begin(), cooked_strings->end(),
[=](const AstRawString* lit) { return lit->IsEmpty(); });
const bool kFirstIsEmpty = first_string->IsEmpty();
Expression* first = kFirstIsEmpty ? ToString(expressions->at(0))
: factory()->NewStringLiteral(
first_string, kNoSourcePosition);
// Build N-ary addition op to simplify code-generation.
// TODO(leszeks): Could we just store this expression in the
// TemplateLiteralState and build it as we go?
NaryOperation* expr = factory()->NewNaryOperation(
Token::ADD, first, 2 * expressions->length() - num_empty);
int i = 0;
if (kFirstIsEmpty) {
// If the first string is empty, possibly add the next template span
// outside of the loop, to keep the loop logic simple.
i = 1;
const AstRawString* str = cooked_strings->at(1);
if (!str->IsEmpty()) {
expr->AddSubsequent(factory()->NewStringLiteral(str, kNoSourcePosition),
first->position());
}
}
while (i < expressions->length()) {
Expression* sub = expressions->at(i++);
const AstRawString* cooked_str = cooked_strings->at(i);
DCHECK_NOT_NULL(cooked_str);
// Let middle be ToString(sub).
expr->AddSubsequent(ToString(sub), sub->position());
if (!cooked_str->IsEmpty()) {
expr->AddSubsequent(
factory()->NewStringLiteral(cooked_str, kNoSourcePosition),
sub->position());
}
}
return expr;
return factory()->NewTemplateLiteral(cooked_strings, expressions, pos);
} else {
// GetTemplateObject
Expression* template_object =
......
......@@ -772,6 +772,7 @@ NOT_A_PATTERN(Spread)
NOT_A_PATTERN(SuperPropertyReference)
NOT_A_PATTERN(SuperCallReference)
NOT_A_PATTERN(SwitchStatement)
NOT_A_PATTERN(TemplateLiteral)
NOT_A_PATTERN(ThisFunction)
NOT_A_PATTERN(Throw)
NOT_A_PATTERN(TryCatchStatement)
......
......@@ -13,20 +13,22 @@ snippet: "
"
frame size: 3
parameter count: 1
bytecode array length: 30
bytecode array length: 28
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 42 S> */ B(LdaSmi), I8(1),
B(Star), R(0),
/* 53 S> */ B(LdaSmi), I8(2),
B(Star), R(1),
/* 56 S> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 56 S> */ B(Ldar), R(0),
B(ToString),
B(Star), R(2),
/* 70 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 70 E> */ B(Add), R(2), U8(0),
B(Ldar), R(1),
/* 70 E> */ B(ToString),
B(Add), R(2), U8(0),
B(Star), R(2),
B(LdaConstant), U8(0),
/* 70 E> */ B(Add), R(2), U8(1),
B(Add), R(2), U8(0),
/* 80 S> */ B(Return),
]
constant pool: [
......@@ -43,7 +45,7 @@ snippet: "
"
frame size: 3
parameter count: 1
bytecode array length: 30
bytecode array length: 28
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 42 S> */ B(LdaSmi), I8(1),
......@@ -52,11 +54,13 @@ bytecodes: [
B(Star), R(1),
/* 56 S> */ B(LdaConstant), U8(0),
B(Star), R(2),
/* 72 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 72 E> */ B(Add), R(2), U8(0),
B(Ldar), R(0),
/* 72 E> */ B(ToString),
B(Add), R(2), U8(0),
B(Star), R(2),
/* 76 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 76 E> */ B(Add), R(2), U8(1),
B(Ldar), R(1),
/* 76 E> */ B(ToString),
B(Add), R(2), U8(0),
/* 80 S> */ B(Return),
]
constant pool: [
......@@ -73,20 +77,22 @@ snippet: "
"
frame size: 3
parameter count: 1
bytecode array length: 30
bytecode array length: 28
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 42 S> */ B(LdaSmi), I8(1),
B(Star), R(0),
/* 53 S> */ B(LdaSmi), I8(2),
B(Star), R(1),
/* 56 S> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 56 S> */ B(Ldar), R(0),
B(ToString),
B(Star), R(2),
B(LdaConstant), U8(0),
/* 66 E> */ B(Add), R(2), U8(0),
B(Add), R(2), U8(0),
B(Star), R(2),
/* 76 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 76 E> */ B(Add), R(2), U8(1),
B(Ldar), R(1),
/* 76 E> */ B(ToString),
B(Add), R(2), U8(0),
/* 80 S> */ B(Return),
]
constant pool: [
......@@ -101,9 +107,9 @@ snippet: "
var b = 2;
return `foo${a}bar${b}baz${1}`;
"
frame size: 4
frame size: 3
parameter count: 1
bytecode array length: 57
bytecode array length: 50
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 42 S> */ B(LdaSmi), I8(1),
......@@ -112,22 +118,23 @@ bytecodes: [
B(Star), R(1),
/* 56 S> */ B(LdaConstant), U8(0),
B(Star), R(2),
/* 69 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 69 E> */ B(Add), R(2), U8(0),
B(Ldar), R(0),
/* 69 E> */ B(ToString),
B(Add), R(2), U8(0),
B(Star), R(2),
B(LdaConstant), U8(1),
/* 69 E> */ B(Add), R(2), U8(1),
B(Add), R(2), U8(0),
B(Star), R(2),
/* 76 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 76 E> */ B(Add), R(2), U8(2),
B(Ldar), R(1),
/* 76 E> */ B(ToString),
B(Add), R(2), U8(0),
B(Star), R(2),
B(LdaConstant), U8(2),
/* 76 E> */ B(Add), R(2), U8(3),
B(Add), R(2), U8(0),
B(Star), R(2),
B(LdaSmi), I8(1),
B(Star), R(3),
B(InvokeIntrinsic), U8(Runtime::k_ToString), R(3), U8(1),
/* 83 E> */ B(Add), R(2), U8(4),
B(ToString),
B(Add), R(2), U8(0),
/* 87 S> */ B(Return),
]
constant pool: [
......@@ -146,23 +153,25 @@ snippet: "
"
frame size: 4
parameter count: 1
bytecode array length: 37
bytecode array length: 35
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 42 S> */ B(LdaSmi), I8(1),
B(Star), R(0),
/* 53 S> */ B(LdaSmi), I8(2),
B(Star), R(1),
/* 56 S> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 56 S> */ B(Ldar), R(0),
B(ToString),
B(Star), R(2),
B(LdaConstant), U8(0),
/* 66 E> */ B(Add), R(2), U8(0),
B(Add), R(2), U8(1),
B(Star), R(2),
B(LdaConstant), U8(0),
B(Star), R(3),
/* 87 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 87 E> */ B(Add), R(3), U8(1),
/* 76 E> */ B(Add), R(2), U8(2),
B(Ldar), R(1),
/* 87 E> */ B(ToString),
B(Add), R(3), U8(2),
/* 76 E> */ B(Add), R(2), U8(0),
/* 91 S> */ B(Return),
]
constant pool: [
......@@ -178,9 +187,9 @@ snippet: "
function foo(a, b) { };
return `string${foo(a, b)}${a}${b}`;
"
frame size: 5
frame size: 4
parameter count: 1
bytecode array length: 52
bytecode array length: 45
bytecodes: [
B(CreateClosure), U8(0), U8(0), U8(2),
B(Star), R(2),
......@@ -191,16 +200,17 @@ bytecodes: [
B(Star), R(1),
/* 80 S> */ B(LdaConstant), U8(1),
B(Star), R(3),
/* 96 E> */ B(CallUndefinedReceiver2), R(2), R(0), R(1), U8(1),
B(Star), R(4),
B(InvokeIntrinsic), U8(Runtime::k_ToString), R(4), U8(1),
/* 96 E> */ B(Add), R(3), U8(3),
/* 96 E> */ B(CallUndefinedReceiver2), R(2), R(0), R(1), U8(2),
B(ToString),
B(Add), R(3), U8(1),
B(Star), R(3),
/* 108 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(0), U8(1),
/* 108 E> */ B(Add), R(3), U8(4),
B(Ldar), R(0),
/* 108 E> */ B(ToString),
B(Add), R(3), U8(1),
B(Star), R(3),
/* 112 E> */ B(InvokeIntrinsic), U8(Runtime::k_ToString), R(1), U8(1),
/* 112 E> */ B(Add), R(3), U8(5),
B(Ldar), R(1),
/* 112 E> */ B(ToString),
B(Add), R(3), U8(1),
/* 116 S> */ B(Return),
]
constant pool: [
......
......@@ -262,7 +262,7 @@ TEST_F(BytecodeArrayBuilderTest, AllBytecodesGenerated) {
.CompareNull();
// Emit conversion operator invocations.
builder.ToNumber(1).ToNumeric(1).ToObject(reg).ToName(reg);
builder.ToNumber(1).ToNumeric(1).ToObject(reg).ToName(reg).ToString();
// Emit GetSuperConstructor.
builder.GetSuperConstructor(reg);
......
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