Commit 05a7d566 by Ian Lance Taylor

compiler, runtime: Use runtime functions to pass closure value.

This changes the compiler and runtime to not pass a closure
value as the last argument, but to instead pass it via
__go_set_closure and retrieve it via __go_get_closure.  This
eliminates the need for function descriptor wrapper functions.
It will make it possible to retrieve the closure value in a
reflect.MakeFunc function.

From-SVN: r202233
parent 3b18bc42
......@@ -1570,14 +1570,6 @@ class Func_descriptor_expression : public Expression
public:
Func_descriptor_expression(Named_object* fn);
// Set the descriptor wrapper.
void
set_descriptor_wrapper(Named_object* dfn)
{
go_assert(this->dfn_ == NULL);
this->dfn_ = dfn;
}
// Make the function descriptor type, so that it can be converted.
static void
make_func_descriptor_type();
......@@ -1594,7 +1586,8 @@ class Func_descriptor_expression : public Expression
{ }
Expression*
do_copy();
do_copy()
{ return Expression::make_func_descriptor(this->fn_); }
bool
do_is_addressable() const
......@@ -1612,8 +1605,6 @@ class Func_descriptor_expression : public Expression
// The function for which this is the descriptor.
Named_object* fn_;
// The descriptor function.
Named_object* dfn_;
// The descriptor variable.
Bvariable* dvar_;
};
......
......@@ -1289,30 +1289,6 @@ Function::get_or_make_decl(Gogo* gogo, Named_object* no, tree id)
functype = TREE_TYPE(TYPE_FIELDS(TREE_TYPE(functype)));
go_assert(FUNCTION_POINTER_TYPE_P(functype));
functype = TREE_TYPE(functype);
// In the struct, the function type always has a trailing
// closure argument. For the function body, we only use
// that trailing arg if this is a function literal or if it
// is a wrapper created to store in a descriptor. Remove it
// in that case.
if (this->enclosing_ == NULL && !this->is_descriptor_wrapper_)
{
tree old_params = TYPE_ARG_TYPES(functype);
go_assert(old_params != NULL_TREE
&& old_params != void_list_node);
tree new_params = NULL_TREE;
tree *pp = &new_params;
while (TREE_CHAIN (old_params) != void_list_node)
{
tree p = TREE_VALUE(old_params);
go_assert(TYPE_P(p));
*pp = tree_cons(NULL_TREE, p, NULL_TREE);
pp = &TREE_CHAIN(*pp);
old_params = TREE_CHAIN (old_params);
}
*pp = void_list_node;
functype = build_function_type(TREE_TYPE(functype), new_params);
}
}
if (functype == error_mark_node)
......@@ -1423,26 +1399,6 @@ Function_declaration::get_or_make_decl(Gogo* gogo, Named_object* no, tree id)
functype = TREE_TYPE(TYPE_FIELDS(TREE_TYPE(functype)));
go_assert(FUNCTION_POINTER_TYPE_P(functype));
functype = TREE_TYPE(functype);
// In the struct, the function type always has a trailing
// closure argument. Here we are referring to the function
// code directly, and we know it is not a function literal,
// and we know it is not a wrapper created to store in a
// descriptor. Remove that trailing argument.
tree old_params = TYPE_ARG_TYPES(functype);
go_assert(old_params != NULL_TREE && old_params != void_list_node);
tree new_params = NULL_TREE;
tree *pp = &new_params;
while (TREE_CHAIN (old_params) != void_list_node)
{
tree p = TREE_VALUE(old_params);
go_assert(TYPE_P(p));
*pp = tree_cons(NULL_TREE, p, NULL_TREE);
pp = &TREE_CHAIN(*pp);
old_params = TREE_CHAIN (old_params);
}
*pp = void_list_node;
functype = build_function_type(TREE_TYPE(functype), new_params);
}
tree decl;
......@@ -1659,8 +1615,13 @@ Function::build_tree(Gogo* gogo, Named_object* named_function)
}
}
// The closure variable is passed last, if this is a function
// literal or a descriptor wrapper.
*pp = NULL_TREE;
DECL_ARGUMENTS(fndecl) = params;
// If we need a closure variable, fetch it by calling a runtime
// function. The caller will have called __go_set_closure before
// the function call.
if (this->closure_var_ != NULL)
{
Bvariable* bvar =
......@@ -1668,25 +1629,25 @@ Function::build_tree(Gogo* gogo, Named_object* named_function)
tree var_decl = var_to_tree(bvar);
if (var_decl != error_mark_node)
{
go_assert(TREE_CODE(var_decl) == PARM_DECL);
*pp = var_decl;
pp = &DECL_CHAIN(*pp);
go_assert(TREE_CODE(var_decl) == VAR_DECL);
static tree get_closure_fndecl;
tree get_closure = Gogo::call_builtin(&get_closure_fndecl,
this->location_,
"__go_get_closure",
0,
ptr_type_node);
// Mark the __go_get_closure function as pure, since it
// depends only on the global variable g.
DECL_PURE_P(get_closure_fndecl) = 1;
get_closure = fold_convert_loc(this->location_.gcc_location(),
TREE_TYPE(var_decl), get_closure);
DECL_INITIAL(var_decl) = get_closure;
DECL_CHAIN(var_decl) = declare_vars;
declare_vars = var_decl;
}
}
else if (this->enclosing_ != NULL || this->is_descriptor_wrapper_)
{
tree parm_decl = build_decl(this->location_.gcc_location(), PARM_DECL,
get_identifier("$closure"),
const_ptr_type_node);
DECL_CONTEXT(parm_decl) = current_function_decl;
DECL_ARG_TYPE(parm_decl) = const_ptr_type_node;
*pp = parm_decl;
pp = &DECL_CHAIN(*pp);
}
*pp = NULL_TREE;
DECL_ARGUMENTS(fndecl) = params;
if (this->block_ != NULL)
{
......
......@@ -1770,8 +1770,8 @@ Create_function_descriptors::function(Named_object* no)
if (no->is_function()
&& no->func_value()->enclosing() == NULL
&& !no->func_value()->is_method()
&& !no->func_value()->is_descriptor_wrapper()
&& !Gogo::is_hidden_name(no->name()))
&& !Gogo::is_hidden_name(no->name())
&& !Gogo::is_thunk(no))
no->func_value()->descriptor(this->gogo_, no);
return TRAVERSE_CONTINUE;
......@@ -2541,13 +2541,38 @@ Order_eval::statement(Block* block, size_t* pindex, Statement* s)
return TRAVERSE_CONTINUE;
// If there is only one expression with a side-effect, we can
// usually leave it in place. However, for an assignment statement,
// we need to evaluate an expression on the right hand side before
// we evaluate any index expression on the left hand side, so for
// that case we always move the expression. Otherwise we mishandle
// m[0] = len(m) where m is a map.
if (c == 1 && s->classification() != Statement::STATEMENT_ASSIGNMENT)
return TRAVERSE_CONTINUE;
// usually leave it in place.
if (c == 1)
{
switch (s->classification())
{
case Statement::STATEMENT_ASSIGNMENT:
// For an assignment statement, we need to evaluate an
// expression on the right hand side before we evaluate any
// index expression on the left hand side, so for that case
// we always move the expression. Otherwise we mishandle
// m[0] = len(m) where m is a map.
break;
case Statement::STATEMENT_EXPRESSION:
{
// If this is a call statement that doesn't return any
// values, it will not have been counted as a value to
// move. We need to move any subexpressions in case they
// are themselves call statements that require passing a
// closure.
Expression* expr = s->expression_statement()->expr();
if (expr->call_expression() != NULL
&& expr->call_expression()->result_count() == 0)
break;
return TRAVERSE_CONTINUE;
}
default:
// We can leave the expression in place.
return TRAVERSE_CONTINUE;
}
}
bool is_thunk = s->thunk_statement() != NULL;
for (Find_eval_ordering::const_iterator p = find_eval_ordering.begin();
......@@ -2803,7 +2828,7 @@ Build_recover_thunks::function(Named_object* orig_no)
Named_object* orig_closure_no = orig_func->closure_var();
Variable* orig_closure_var = orig_closure_no->var_value();
Variable* new_var = new Variable(orig_closure_var->type(), NULL, false,
true, false, location);
false, false, location);
snprintf(buf, sizeof buf, "closure.%u", count);
++count;
Named_object* new_closure_no = Named_object::make_variable(buf, NULL,
......@@ -3275,7 +3300,7 @@ Function::Function(Function_type* type, Function* enclosing, Block* block,
local_type_count_(0), descriptor_(NULL), fndecl_(NULL), defer_stack_(NULL),
is_sink_(false), results_are_named_(false), nointerface_(false),
calls_recover_(false), is_recover_thunk_(false), has_recover_thunk_(false),
in_unique_section_(false), is_descriptor_wrapper_(false)
in_unique_section_(false)
{
}
......@@ -3357,9 +3382,9 @@ Function::closure_var()
Struct_field_list* sfl = new Struct_field_list;
Type* struct_type = Type::make_struct_type(sfl, loc);
Variable* var = new Variable(Type::make_pointer_type(struct_type),
NULL, false, true, false, loc);
NULL, false, false, false, loc);
var->set_is_used();
this->closure_var_ = Named_object::make_variable("closure", NULL, var);
this->closure_var_ = Named_object::make_variable("$closure", NULL, var);
// Note that the new variable is not in any binding contour.
}
return this->closure_var_;
......@@ -3562,99 +3587,16 @@ Function::determine_types()
this->block_->determine_types();
}
// Build a wrapper function for a function descriptor. A function
// descriptor refers to a function that takes a closure as its last
// argument. In this case there will be no closure, but an indirect
// call will pass nil as the last argument. We need to build a
// wrapper function that accepts and discards that last argument, so
// that cases like -mrtd will work correctly. In most cases the
// wrapper function will simply be a jump.
Named_object*
Function::make_descriptor_wrapper(Gogo* gogo, Named_object* no,
Function_type* orig_fntype)
{
Location loc = no->location();
Type* vt = Type::make_pointer_type(Type::make_void_type());
Function_type* new_fntype = orig_fntype->copy_with_closure(vt);
std::string name = no->name() + "$descriptorfn";
Named_object* dno = gogo->start_function(name, new_fntype, false, loc);
dno->func_value()->is_descriptor_wrapper_ = true;
// Put the wrapper in a unique section so that it can be discarded
// by the linker if it is not needed. Every top-level function will
// get a wrapper, in case there is a reference other than a call
// from some other package, but most will not need one.
dno->func_value()->set_in_unique_section();
gogo->start_block(loc);
Expression* fn = Expression::make_func_reference(no, NULL, loc);
// Call the function begin wrapped, passing all of the arguments
// except for the last one (the last argument is the ignored
// closure).
const Typed_identifier_list* orig_params = orig_fntype->parameters();
Expression_list* args;
if (orig_params == NULL || orig_params->empty())
args = NULL;
else
{
const Typed_identifier_list* new_params = new_fntype->parameters();
args = new Expression_list();
for (Typed_identifier_list::const_iterator p = new_params->begin();
p + 1 != new_params->end();
++p)
{
Named_object* p_no = gogo->lookup(p->name(), NULL);
go_assert(p_no != NULL
&& p_no->is_variable()
&& p_no->var_value()->is_parameter());
args->push_back(Expression::make_var_reference(p_no, loc));
}
}
Call_expression* call = Expression::make_call(fn, args,
orig_fntype->is_varargs(),
loc);
call->set_varargs_are_lowered();
Statement* s = Statement::make_return_from_call(call, loc);
gogo->add_statement(s);
Block* b = gogo->finish_block(loc);
gogo->add_block(b, loc);
gogo->lower_block(dno, b);
gogo->finish_function(loc);
return dno;
}
// Return the function descriptor, the value you get when you refer to
// the function in Go code without calling it.
Expression*
Function::descriptor(Gogo* gogo, Named_object* no)
Function::descriptor(Gogo*, Named_object* no)
{
go_assert(!this->is_method());
go_assert(this->closure_var_ == NULL);
go_assert(!this->is_descriptor_wrapper_);
if (this->descriptor_ == NULL)
{
// Make and record the descriptor first, so that when we lower
// the descriptor wrapper we don't try to make it again.
Func_descriptor_expression* descriptor =
Expression::make_func_descriptor(no);
this->descriptor_ = descriptor;
if (no->package() == NULL
&& !Linemap::is_predeclared_location(no->location()))
{
Named_object* dno = Function::make_descriptor_wrapper(gogo, no,
this->type_);
descriptor->set_descriptor_wrapper(dno);
}
}
this->descriptor_ = Expression::make_func_descriptor(no);
return this->descriptor_;
}
......@@ -4193,24 +4135,11 @@ Bindings_snapshot::check_goto_defs(Location loc, const Block* block,
// Return the function descriptor.
Expression*
Function_declaration::descriptor(Gogo* gogo, Named_object* no)
Function_declaration::descriptor(Gogo*, Named_object* no)
{
go_assert(!this->fntype_->is_method());
if (this->descriptor_ == NULL)
{
// Make and record the descriptor first, so that when we lower
// the descriptor wrapper we don't try to make it again.
Func_descriptor_expression* descriptor =
Expression::make_func_descriptor(no);
this->descriptor_ = descriptor;
if (no->package() == NULL
&& !Linemap::is_predeclared_location(no->location()))
{
Named_object* dno = Function::make_descriptor_wrapper(gogo, no,
this->fntype_);
descriptor->set_descriptor_wrapper(dno);
}
}
this->descriptor_ = Expression::make_func_descriptor(no);
return this->descriptor_;
}
......
......@@ -1050,12 +1050,6 @@ class Function
set_in_unique_section()
{ this->in_unique_section_ = true; }
// Whether this function was created as a descriptor wrapper for
// another function.
bool
is_descriptor_wrapper() const
{ return this->is_descriptor_wrapper_; }
// Swap with another function. Used only for the thunk which calls
// recover.
void
......@@ -1085,10 +1079,6 @@ class Function
this->descriptor_ = descriptor;
}
// Build a descriptor wrapper function.
static Named_object*
make_descriptor_wrapper(Gogo*, Named_object*, Function_type*);
// Return the function's decl given an identifier.
tree
get_or_make_decl(Gogo*, Named_object*, tree id);
......@@ -1190,9 +1180,6 @@ class Function
// True if this function should be put in a unique section. This is
// turned on for field tracking.
bool in_unique_section_ : 1;
// True if this is a function wrapper created to put in a function
// descriptor.
bool is_descriptor_wrapper_ : 1;
};
// A snapshot of the current binding state.
......
......@@ -1658,46 +1658,23 @@ Statement::make_tuple_type_guard_assignment(Expression* val, Expression* ok,
location);
}
// An expression statement.
// Class Expression_statement.
class Expression_statement : public Statement
{
public:
Expression_statement(Expression* expr, bool is_ignored)
: Statement(STATEMENT_EXPRESSION, expr->location()),
expr_(expr), is_ignored_(is_ignored)
{ }
Expression*
expr()
{ return this->expr_; }
protected:
int
do_traverse(Traverse* traverse)
{ return this->traverse_expression(traverse, &this->expr_); }
void
do_determine_types()
{ this->expr_->determine_type_no_context(); }
void
do_check_types(Gogo*);
bool
do_may_fall_through() const;
// Constructor.
Bstatement*
do_get_backend(Translate_context* context);
Expression_statement::Expression_statement(Expression* expr, bool is_ignored)
: Statement(STATEMENT_EXPRESSION, expr->location()),
expr_(expr), is_ignored_(is_ignored)
{
}
void
do_dump_statement(Ast_dump_context*) const;
// Determine types.
private:
Expression* expr_;
// Whether the value of this expression is being explicitly ignored.
bool is_ignored_;
};
void
Expression_statement::do_determine_types()
{
this->expr_->determine_type_no_context();
}
// Check the types of an expression statement. The only check we do
// is to possibly give an error about discarding the value of the
......
......@@ -17,6 +17,7 @@ class Function;
class Unnamed_label;
class Temporary_statement;
class Variable_declaration_statement;
class Expression_statement;
class Return_statement;
class Thunk_statement;
class Label_statement;
......@@ -329,6 +330,14 @@ class Statement
STATEMENT_VARIABLE_DECLARATION>();
}
// If this is an expression statement, return it. Otherwise return
// NULL.
Expression_statement*
expression_statement()
{
return this->convert<Expression_statement, STATEMENT_EXPRESSION>();
}
// If this is a return statement, return it. Otherwise return NULL.
Return_statement*
return_statement()
......@@ -636,6 +645,43 @@ class Return_statement : public Statement
bool is_lowered_;
};
// An expression statement.
class Expression_statement : public Statement
{
public:
Expression_statement(Expression* expr, bool is_ignored);
Expression*
expr()
{ return this->expr_; }
protected:
int
do_traverse(Traverse* traverse)
{ return this->traverse_expression(traverse, &this->expr_); }
void
do_determine_types();
void
do_check_types(Gogo*);
bool
do_may_fall_through() const;
Bstatement*
do_get_backend(Translate_context* context);
void
do_dump_statement(Ast_dump_context*) const;
private:
Expression* expr_;
// Whether the value of this expression is being explicitly ignored.
bool is_ignored_;
};
// A send statement.
class Send_statement : public Statement
......
......@@ -3390,10 +3390,7 @@ Function_type::do_get_backend(Gogo* gogo)
// When we do anything with a function value other than call it, it
// is represented as a pointer to a struct whose first field is the
// actual function. So that is what we return as the type of a Go
// function. The function stored in the first field always that
// takes one additional trailing argument: the closure pointer. For
// a top-level function, this additional argument will only be
// passed when invoking the function indirectly, via the struct.
// function.
Location loc = this->location();
Btype* struct_type =
......@@ -3415,15 +3412,9 @@ Function_type::do_get_backend(Gogo* gogo)
}
std::vector<Backend::Btyped_identifier> bparameters;
size_t last;
if (this->parameters_ == NULL)
{
bparameters.resize(1);
last = 0;
}
else
if (this->parameters_ != NULL)
{
bparameters.resize(this->parameters_->size() + 1);
bparameters.resize(this->parameters_->size());
size_t i = 0;
for (Typed_identifier_list::const_iterator p = this->parameters_->begin();
p != this->parameters_->end();
......@@ -3433,12 +3424,8 @@ Function_type::do_get_backend(Gogo* gogo)
bparameters[i].btype = p->type()->get_backend(gogo);
bparameters[i].location = p->location();
}
last = i;
go_assert(i == bparameters.size());
}
go_assert(last + 1 == bparameters.size());
bparameters[last].name = "$closure";
bparameters[last].btype = ptr_struct_type;
bparameters[last].location = loc;
std::vector<Backend::Btyped_identifier> bresults;
if (this->results_ != NULL)
......@@ -3840,7 +3827,7 @@ Function_type::copy_with_receiver(Type* receiver_type) const
// closure parameter.
Function_type*
Function_type::copy_with_closure(Type* closure_type) const
Function_type::copy_with_names() const
{
Typed_identifier_list* new_params = new Typed_identifier_list();
const Typed_identifier_list* orig_params = this->parameters_;
......@@ -3858,8 +3845,6 @@ Function_type::copy_with_closure(Type* closure_type) const
p->location()));
}
}
new_params->push_back(Typed_identifier("closure.0", closure_type,
this->location_));
const Typed_identifier_list* orig_results = this->results_;
Typed_identifier_list* new_results;
......
......@@ -1789,11 +1789,11 @@ class Function_type : public Type
Function_type*
copy_with_receiver(Type*) const;
// Return a copy of this type ignoring any receiver and adding a
// final closure parameter of type CLOSURE_TYPE. This is used when
// creating descriptors.
// Return a copy of this type ignoring any receiver and using dummy
// names for all parameters. This is used for thunks for method
// values.
Function_type*
copy_with_closure(Type* closure_type) const;
copy_with_names() const;
static Type*
make_function_type_descriptor_type();
......
......@@ -434,9 +434,6 @@ func (v Value) call(op string, in []Value) []Value {
nin++
}
firstPointer := len(in) > 0 && Kind(t.In(0).(*rtype).kind) != Ptr && v.flag&flagMethod == 0 && isMethod(v.typ)
if v.flag&flagMethod == 0 && !firstPointer {
nin++
}
params := make([]unsafe.Pointer, nin)
off := 0
if v.flag&flagMethod != 0 {
......@@ -464,10 +461,6 @@ func (v Value) call(op string, in []Value) []Value {
}
off++
}
if v.flag&flagMethod == 0 && !firstPointer {
// Closure argument.
params[off] = unsafe.Pointer(&fn)
}
ret := make([]Value, nout)
results := make([]unsafe.Pointer, nout)
......
......@@ -302,9 +302,7 @@ go_func_to_cif (const struct __go_func_type *func, _Bool is_interface,
in_types = ((const struct __go_type_descriptor **)
func->__in.__values);
num_args = (num_params
+ (is_interface ? 1 : 0)
+ (!is_interface && !is_method ? 1 : 0));
num_args = num_params + (is_interface ? 1 : 0);
args = (ffi_type **) __go_alloc (num_args * sizeof (ffi_type *));
i = 0;
off = 0;
......@@ -321,12 +319,6 @@ go_func_to_cif (const struct __go_func_type *func, _Bool is_interface,
for (; i < num_params; ++i)
args[i + off] = go_type_to_ffi (in_types[i]);
if (!is_interface && !is_method)
{
// There is a closure argument, a pointer.
args[i + off] = &ffi_type_pointer;
}
rettype = go_func_return_ffi (func);
status = ffi_prep_cif (cif, FFI_DEFAULT_ABI, num_args, rettype, args);
......@@ -511,9 +503,8 @@ go_set_results (const struct __go_func_type *func, unsigned char *call_result,
regardless of FUNC_TYPE, it is passed as a pointer.
If neither IS_INTERFACE nor IS_METHOD is true then we are calling a
function indirectly, and the caller is responsible for passing a
trailing closure argument, a pointer, which is not described in
FUNC_TYPE. */
function indirectly, and we must pass a closure pointer via
__go_set_closure. The pointer to pass is simply FUNC_VAL. */
void
reflect_call (const struct __go_func_type *func_type, FuncVal *func_val,
......@@ -528,6 +519,8 @@ reflect_call (const struct __go_func_type *func_type, FuncVal *func_val,
call_result = (unsigned char *) malloc (go_results_size (func_type));
if (!is_interface && !is_method)
__go_set_closure (func_val);
ffi_call (&cif, func_val->fn, call_result, params);
/* Some day we may need to free result values if RESULTS is
......
......@@ -2263,12 +2263,11 @@ runfinq(void* dummy __attribute__ ((unused)))
for(; fb; fb=next) {
next = fb->next;
for(i=0; i<(uint32)fb->cnt; i++) {
void *params[2];
void *param;
f = &fb->fin[i];
params[0] = &f->arg;
params[1] = f;
reflect_call(f->ft, f->fn, 0, 0, params, nil);
param = &f->arg;
reflect_call(f->ft, f->fn, 0, 0, &param, nil);
f->fn = nil;
f->arg = nil;
}
......
......@@ -2832,3 +2832,23 @@ runtime_proc_scan(void (*addroot)(Obj))
{
addroot((Obj){(byte*)&runtime_sched, sizeof runtime_sched, 0});
}
// When a function calls a closure, it passes the closure value to
// __go_set_closure immediately before the function call. When a
// function uses a closure, it calls __go_get_closure immediately on
// function entry. This is a hack, but it will work on any system.
// It would be better to use the static chain register when there is
// one. It is also worth considering expanding these functions
// directly in the compiler.
void
__go_set_closure(void* v)
{
g->closure = v;
}
void *
__go_get_closure(void)
{
return g->closure;
}
......@@ -190,6 +190,7 @@ struct Location
struct G
{
void* closure; // Closure value.
Defer* defer;
Panic* panic;
void* exception; // current exception being thrown
......@@ -759,3 +760,6 @@ extern void runtime_main(void*);
int32 getproccount(void);
#define PREFETCH(p) __builtin_prefetch(p)
void __go_set_closure(void*);
void* __go_get_closure(void);
......@@ -46,10 +46,9 @@ static void siftdown(int32);
// Ready the goroutine e.data.
static void
ready(int64 now, Eface e, void *closure)
ready(int64 now, Eface e)
{
USED(now);
USED(closure);
runtime_ready(e.__object);
}
......@@ -166,7 +165,7 @@ timerproc(void* dummy __attribute__ ((unused)))
{
int64 delta, now;
Timer *t;
void (*f)(int64, Eface, void *);
void (*f)(int64, Eface);
Eface arg;
for(;;) {
......@@ -197,7 +196,8 @@ timerproc(void* dummy __attribute__ ((unused)))
runtime_unlock(&timers);
if(raceenabled)
runtime_raceacquire(t);
f(now, arg, &t->fv);
__go_set_closure(t->fv);
f(now, arg);
runtime_lock(&timers);
}
if(delta < 0) {
......
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