Commit f65cf2b7 by Martin Jambor Committed by Martin Jambor

re PR tree-optimization/45934 (g++.old-deja/g++.other/dtor5.C FAILs with -finline-small-functions)

2011-01-14  Martin Jambor  <mjambor@suse.cz>

	PR tree-optimization/45934
	PR tree-optimization/46302
	* ipa-prop.c (type_change_info): New type.
	(stmt_may_be_vtbl_ptr_store): New function.
	(check_stmt_for_type_change): Likewise.
	(detect_type_change): Likewise.
	(detect_type_change_ssa): Likewise.
	(compute_complex_assign_jump_func): Check for dynamic type change.
	(compute_complex_ancestor_jump_func): Likewise.
	(compute_known_type_jump_func): Likewise.
	(compute_scalar_jump_functions): Likewise.
	(ipa_analyze_virtual_call_uses): Likewise.
	(ipa_analyze_node): Push and pop cfun, set current_function_decl.

	* testsuite/g++.dg/ipa/devirt-c-1.C: New test.
	* testsuite/g++.dg/ipa/devirt-c-2.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-c-3.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-c-4.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-c-5.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-c-6.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-6.C: Likewise.
	* testsuite/g++.dg/ipa/devirt-d-1.C: Likewise.
	* testsuite/g++.dg/torture/pr45934.C: Likewise.

From-SVN: r168825
parent dc0d2dae
2011-01-14 Martin Jambor <mjambor@suse.cz>
PR tree-optimization/45934
PR tree-optimization/46302
* ipa-prop.c (type_change_info): New type.
(stmt_may_be_vtbl_ptr_store): New function.
(check_stmt_for_type_change): Likewise.
(detect_type_change): Likewise.
(detect_type_change_ssa): Likewise.
(compute_complex_assign_jump_func): Check for dynamic type change.
(compute_complex_ancestor_jump_func): Likewise.
(compute_known_type_jump_func): Likewise.
(compute_scalar_jump_functions): Likewise.
(ipa_analyze_virtual_call_uses): Likewise.
(ipa_analyze_node): Push and pop cfun, set current_function_decl.
2011-01-14 Joseph Myers <joseph@codesourcery.com>
* config/i386/i386.h (CC1_CPU_SPEC_1): Don't handle -msse5.
......
......@@ -350,6 +350,153 @@ ipa_print_all_jump_functions (FILE *f)
}
}
/* Structure to be passed in between detect_type_change and
check_stmt_for_type_change. */
struct type_change_info
{
/* Set to true if dynamic type change has been detected. */
bool type_maybe_changed;
};
/* Return true if STMT can modify a virtual method table pointer.
This function makes special assumptions about both constructors and
destructors which are all the functions that are allowed to alter the VMT
pointers. It assumes that destructors begin with assignment into all VMT
pointers and that constructors essentially look in the following way:
1) The very first thing they do is that they call constructors of ancestor
sub-objects that have them.
2) Then VMT pointers of this and all its ancestors is set to new values
corresponding to the type corresponding to the constructor.
3) Only afterwards, other stuff such as constructor of member sub-objects
and the code written by the user is run. Only this may include calling
virtual functions, directly or indirectly.
There is no way to call a constructor of an ancestor sub-object in any
other way.
This means that we do not have to care whether constructors get the correct
type information because they will always change it (in fact, if we define
the type to be given by the VMT pointer, it is undefined).
The most important fact to derive from the above is that if, for some
statement in the section 3, we try to detect whether the dynamic type has
changed, we can safely ignore all calls as we examine the function body
backwards until we reach statements in section 2 because these calls cannot
be ancestor constructors or destructors (if the input is not bogus) and so
do not change the dynamic type (this holds true only for automatically
allocated objects but at the moment we devirtualize only these). We then
must detect that statements in section 2 change the dynamic type and can try
to derive the new type. That is enough and we can stop, we will never see
the calls into constructors of sub-objects in this code. Therefore we can
safely ignore all call statements that we traverse.
*/
static bool
stmt_may_be_vtbl_ptr_store (gimple stmt)
{
if (is_gimple_call (stmt))
return false;
else if (is_gimple_assign (stmt))
{
tree lhs = gimple_assign_lhs (stmt);
if (TREE_CODE (lhs) == COMPONENT_REF
&& !DECL_VIRTUAL_P (TREE_OPERAND (lhs, 1))
&& !AGGREGATE_TYPE_P (TREE_TYPE (lhs)))
return false;
/* In the future we might want to use get_base_ref_and_offset to find
if there is a field corresponding to the offset and if so, proceed
almost like if it was a component ref. */
}
return true;
}
/* Callbeck of walk_aliased_vdefs and a helper function for
detect_type_change to check whether a particular statement may modify
the virtual table pointer, and if possible also determine the new type of
the (sub-)object. It stores its result into DATA, which points to a
type_change_info structure. */
static bool
check_stmt_for_type_change (ao_ref *ao ATTRIBUTE_UNUSED, tree vdef, void *data)
{
gimple stmt = SSA_NAME_DEF_STMT (vdef);
struct type_change_info *tci = (struct type_change_info *) data;
if (stmt_may_be_vtbl_ptr_store (stmt))
{
tci->type_maybe_changed = true;
return true;
}
else
return false;
}
/* Detect whether the dynamic type of ARG has changed (before callsite CALL) by
looking for assignments to its virtual table pointer. If it is, return true
and fill in the jump function JFUNC with relevant type information or set it
to unknown. ARG is the object itself (not a pointer to it, unless
dereferenced). BASE is the base of the memory access as returned by
get_ref_base_and_extent, as is the offset. */
static bool
detect_type_change (tree arg, tree base, gimple call,
struct ipa_jump_func *jfunc, HOST_WIDE_INT offset)
{
struct type_change_info tci;
ao_ref ao;
gcc_checking_assert (DECL_P (arg)
|| TREE_CODE (arg) == MEM_REF
|| handled_component_p (arg));
/* Const calls cannot call virtual methods through VMT and so type changes do
not matter. */
if (!gimple_vuse (call))
return false;
tci.type_maybe_changed = false;
ao.ref = arg;
ao.base = base;
ao.offset = offset;
ao.size = POINTER_SIZE;
ao.max_size = ao.size;
ao.ref_alias_set = -1;
ao.base_alias_set = -1;
walk_aliased_vdefs (&ao, gimple_vuse (call), check_stmt_for_type_change,
&tci, NULL);
if (!tci.type_maybe_changed)
return false;
jfunc->type = IPA_JF_UNKNOWN;
return true;
}
/* Like detect_type_change but ARG is supposed to be a non-dereferenced pointer
SSA name (its dereference will become the base and the offset is assumed to
be zero). */
static bool
detect_type_change_ssa (tree arg, gimple call, struct ipa_jump_func *jfunc)
{
gcc_checking_assert (TREE_CODE (arg) == SSA_NAME);
if (!POINTER_TYPE_P (TREE_TYPE (arg))
|| TREE_CODE (TREE_TYPE (TREE_TYPE (arg))) != RECORD_TYPE)
return false;
arg = build2 (MEM_REF, ptr_type_node, arg,
build_int_cst (ptr_type_node, 0));
return detect_type_change (arg, arg, call, jfunc, 0);
}
/* Given that an actual argument is an SSA_NAME (given in NAME) and is a result
of an assignment statement STMT, try to find out whether NAME can be
described by a (possibly polynomial) pass-through jump-function or an
......@@ -359,10 +506,10 @@ ipa_print_all_jump_functions (FILE *f)
static void
compute_complex_assign_jump_func (struct ipa_node_params *info,
struct ipa_jump_func *jfunc,
gimple stmt, tree name)
gimple call, gimple stmt, tree name)
{
HOST_WIDE_INT offset, size, max_size;
tree op1, op2, base, type;
tree op1, op2, base, ssa;
int index;
op1 = gimple_assign_rhs1 (stmt);
......@@ -388,7 +535,8 @@ compute_complex_assign_jump_func (struct ipa_node_params *info,
jfunc->value.pass_through.operation = gimple_assign_rhs_code (stmt);
jfunc->value.pass_through.operand = op2;
}
else if (gimple_assign_unary_nop_p (stmt))
else if (gimple_assign_unary_nop_p (stmt)
&& !detect_type_change_ssa (op1, call, jfunc))
{
jfunc->type = IPA_JF_PASS_THROUGH;
jfunc->value.pass_through.formal_id = index;
......@@ -399,10 +547,8 @@ compute_complex_assign_jump_func (struct ipa_node_params *info,
if (TREE_CODE (op1) != ADDR_EXPR)
return;
op1 = TREE_OPERAND (op1, 0);
type = TREE_TYPE (op1);
if (TREE_CODE (type) != RECORD_TYPE)
if (TREE_CODE (TREE_TYPE (op1)) != RECORD_TYPE)
return;
base = get_ref_base_and_extent (op1, &offset, &size, &max_size);
if (TREE_CODE (base) != MEM_REF
......@@ -411,20 +557,21 @@ compute_complex_assign_jump_func (struct ipa_node_params *info,
|| max_size != size)
return;
offset += mem_ref_offset (base).low * BITS_PER_UNIT;
base = TREE_OPERAND (base, 0);
if (TREE_CODE (base) != SSA_NAME
|| !SSA_NAME_IS_DEFAULT_DEF (base)
ssa = TREE_OPERAND (base, 0);
if (TREE_CODE (ssa) != SSA_NAME
|| !SSA_NAME_IS_DEFAULT_DEF (ssa)
|| offset < 0)
return;
/* Dynamic types are changed only in constructors and destructors and */
index = ipa_get_param_decl_index (info, SSA_NAME_VAR (base));
if (index >= 0)
index = ipa_get_param_decl_index (info, SSA_NAME_VAR (ssa));
if (index >= 0
&& !detect_type_change (op1, base, call, jfunc, offset))
{
jfunc->type = IPA_JF_ANCESTOR;
jfunc->value.ancestor.formal_id = index;
jfunc->value.ancestor.offset = offset;
jfunc->value.ancestor.type = type;
jfunc->value.ancestor.type = TREE_TYPE (op1);
}
}
......@@ -453,12 +600,12 @@ compute_complex_assign_jump_func (struct ipa_node_params *info,
static void
compute_complex_ancestor_jump_func (struct ipa_node_params *info,
struct ipa_jump_func *jfunc,
gimple phi)
gimple call, gimple phi)
{
HOST_WIDE_INT offset, size, max_size;
gimple assign, cond;
basic_block phi_bb, assign_bb, cond_bb;
tree tmp, parm, expr;
tree tmp, parm, expr, obj;
int index, i;
if (gimple_phi_num_args (phi) != 2)
......@@ -486,6 +633,7 @@ compute_complex_ancestor_jump_func (struct ipa_node_params *info,
if (TREE_CODE (expr) != ADDR_EXPR)
return;
expr = TREE_OPERAND (expr, 0);
obj = expr;
expr = get_ref_base_and_extent (expr, &offset, &size, &max_size);
if (TREE_CODE (expr) != MEM_REF
......@@ -513,7 +661,6 @@ compute_complex_ancestor_jump_func (struct ipa_node_params *info,
|| !integer_zerop (gimple_cond_rhs (cond)))
return;
phi_bb = gimple_bb (phi);
for (i = 0; i < 2; i++)
{
......@@ -522,10 +669,13 @@ compute_complex_ancestor_jump_func (struct ipa_node_params *info,
return;
}
jfunc->type = IPA_JF_ANCESTOR;
jfunc->value.ancestor.formal_id = index;
jfunc->value.ancestor.offset = offset;
jfunc->value.ancestor.type = TREE_TYPE (TREE_TYPE (tmp));
if (!detect_type_change (obj, expr, call, jfunc, offset))
{
jfunc->type = IPA_JF_ANCESTOR;
jfunc->value.ancestor.formal_id = index;
jfunc->value.ancestor.offset = offset;
jfunc->value.ancestor.type = TREE_TYPE (obj);;
}
}
/* Given OP whch is passed as an actual argument to a called function,
......@@ -533,7 +683,8 @@ compute_complex_ancestor_jump_func (struct ipa_node_params *info,
and if so, create one and store it to JFUNC. */
static void
compute_known_type_jump_func (tree op, struct ipa_jump_func *jfunc)
compute_known_type_jump_func (tree op, struct ipa_jump_func *jfunc,
gimple call)
{
HOST_WIDE_INT offset, size, max_size;
tree base, binfo;
......@@ -551,6 +702,9 @@ compute_known_type_jump_func (tree op, struct ipa_jump_func *jfunc)
|| is_global_var (base))
return;
if (detect_type_change (op, base, call, jfunc, offset))
return;
binfo = TYPE_BINFO (TREE_TYPE (base));
if (!binfo)
return;
......@@ -592,7 +746,8 @@ compute_scalar_jump_functions (struct ipa_node_params *info,
{
int index = ipa_get_param_decl_index (info, SSA_NAME_VAR (arg));
if (index >= 0)
if (index >= 0
&& !detect_type_change_ssa (arg, call, &functions[num]))
{
functions[num].type = IPA_JF_PASS_THROUGH;
functions[num].value.pass_through.formal_id = index;
......@@ -604,14 +759,14 @@ compute_scalar_jump_functions (struct ipa_node_params *info,
gimple stmt = SSA_NAME_DEF_STMT (arg);
if (is_gimple_assign (stmt))
compute_complex_assign_jump_func (info, &functions[num],
stmt, arg);
call, stmt, arg);
else if (gimple_code (stmt) == GIMPLE_PHI)
compute_complex_ancestor_jump_func (info, &functions[num],
stmt);
call, stmt);
}
}
else
compute_known_type_jump_func (arg, &functions[num]);
compute_known_type_jump_func (arg, &functions[num], call);
}
}
......@@ -1218,6 +1373,7 @@ ipa_analyze_virtual_call_uses (struct cgraph_node *node,
struct ipa_node_params *info, gimple call,
tree target)
{
struct ipa_jump_func jfunc;
tree obj = OBJ_TYPE_REF_OBJECT (target);
tree var;
int index;
......@@ -1241,7 +1397,8 @@ ipa_analyze_virtual_call_uses (struct cgraph_node *node,
var = SSA_NAME_VAR (obj);
index = ipa_get_param_decl_index (info, var);
if (index >= 0)
if (index >= 0
&& !detect_type_change_ssa (obj, call, &jfunc))
ipa_note_param_call (node, index, call, true);
}
......@@ -1364,6 +1521,8 @@ ipa_analyze_node (struct cgraph_node *node)
struct param_analysis_info *parms_info;
int i, param_count;
push_cfun (DECL_STRUCT_FUNCTION (node->decl));
current_function_decl = node->decl;
ipa_initialize_node_params (node);
param_count = ipa_get_param_count (info);
......@@ -1376,6 +1535,9 @@ ipa_analyze_node (struct cgraph_node *node)
for (i = 0; i < param_count; i++)
if (parms_info[i].visited_statements)
BITMAP_FREE (parms_info[i].visited_statements);
current_function_decl = NULL;
pop_cfun ();
}
......
2011-01-14 Martin Jambor <mjambor@suse.cz>
PR tree-optimization/45934
PR tree-optimization/46302
* g++.dg/ipa/devirt-c-1.C: New test.
* g++.dg/ipa/devirt-c-2.C: Likewise.
* g++.dg/ipa/devirt-c-3.C: Likewise.
* g++.dg/ipa/devirt-c-4.C: Likewise.
* g++.dg/ipa/devirt-c-5.C: Likewise.
* g++.dg/ipa/devirt-c-6.C: Likewise.
* g++.dg/ipa/devirt-6.C: Likewise.
* g++.dg/ipa/devirt-d-1.C: Likewise.
* g++.dg/torture/pr45934.C: Likewise.
2011-01-14 Jason Merrill <jason@redhat.com>
* g++.dg/cpp0x/variadic105.C: New.
......
/* Verify that we either do not do any devirtualization or correctly
spot that foo changes the dynamic type of the passed object. */
/* { dg-do run } */
/* { dg-options "-O3" } */
extern "C" void abort (void);
extern "C" void *malloc(__SIZE_TYPE__);
inline void* operator new(__SIZE_TYPE__, void* __p) throw() { return __p;}
int x;
class A {
public:
virtual ~A() { }
};
class B : public A {
public:
virtual ~B() { if (x == 1) abort (); x = 1; }
};
void __attribute__((noinline,noclone)) foo (void *p)
{
B *b = reinterpret_cast<B *>(p);
b->~B();
new (p) A;
}
int main()
{
void *p = __builtin_malloc (sizeof (B));
new (p) B;
foo(p);
reinterpret_cast<A *>(p)->~A();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-early-inlining -fno-inline" } */
extern "C" void abort (void);
class A
{
public:
int data;
A();
virtual int foo (int i);
};
class B : public A
{
public:
virtual int foo (int i);
};
class C : public A
{
public:
virtual int foo (int i);
};
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int C::foo (int i)
{
return i + 3;
}
static int middleman (class A *obj, int i)
{
return obj->foo (i);
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
A::A ()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
static void bah ()
{
class B b;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-early-inlining -fno-inline" } */
extern "C" void abort (void);
class Distraction
{
public:
float f;
double d;
Distraction ()
{
f = 8.3;
d = 10.2;
}
virtual float bar (float z);
};
class A
{
public:
int data;
A();
virtual int foo (int i);
};
class B : public Distraction, public A
{
public:
virtual int foo (int i);
};
float Distraction::bar (float z)
{
f += z;
return f/2;
}
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
static int middleman (class A *obj, int i)
{
return obj->foo (i);
}
A::A()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
static void bah ()
{
class B b;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-inline" } */
extern "C" void abort (void);
class Distraction
{
public:
float f;
double d;
Distraction ()
{
f = 8.3;
d = 10.2;
}
virtual float bar (float z);
};
class A
{
public:
int data;
A();
virtual int foo (int i);
};
class B : public Distraction, public A
{
public:
virtual int foo (int i);
};
float Distraction::bar (float z)
{
f += z;
return f/2;
}
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
static int __attribute__ ((noinline))
middleman (class A *obj, int i)
{
return obj->foo (i);
}
inline __attribute__ ((always_inline)) A::A()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
static void bah ()
{
class B b;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-inline" } */
extern "C" void abort (void);
class Distraction
{
public:
float f;
double d;
Distraction ()
{
f = 8.3;
d = 10.2;
}
virtual float bar (float z);
};
class A
{
public:
int data;
A();
virtual int foo (int i);
};
class B : public Distraction, public A
{
public:
B();
virtual int foo (int i);
};
class C : public B
{
public:
virtual int foo (int i);
};
float Distraction::bar (float z)
{
f += z;
return f/2;
}
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int C::foo (int i)
{
return i + 3;
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
static int __attribute__ ((noinline))
middleman (class A *obj, int i)
{
return obj->foo (i);
}
static void __attribute__ ((noinline))
sth2 (A *a)
{
if (a->foo (get_input ()) != 3)
abort ();
}
inline void __attribute__ ((always_inline)) sth1 (B *b)
{
sth2 (b);
}
inline __attribute__ ((always_inline)) A::A()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
B::B() : Distraction(), A()
{
sth1 (this);
}
static void bah ()
{
class C c;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-early-inlining -fno-inline" } */
extern "C" void abort (void);
class B;
class A
{
public:
int data;
A();
A(B *b);
virtual int foo (int i);
};
class B : public A
{
public:
virtual int foo (int i);
};
class C : public A
{
public:
virtual int foo (int i);
};
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int C::foo (int i)
{
return i + 3;
}
static int middleman (class A *obj, int i)
{
return obj->foo (i);
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
A::A ()
{
}
A::A (B *b)
{
if (middleman (b, get_input ()) != 3)
abort ();
}
static void bah ()
{
B b;
A a(&b);
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under construction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-inline" } */
extern "C" void abort (void);
class A
{
public:
int data;
A();
virtual int foo (int i);
};
class B : public A
{
public:
virtual int foo (int i);
};
class C : public A
{
public:
virtual int foo (int i);
};
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int C::foo (int i)
{
return i + 3;
}
static inline int __attribute__ ((always_inline))
middleman (class A *obj, int i)
{
return obj->foo (i);
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
__attribute__ ((noinline)) A::A ()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
static void bah ()
{
class B b;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* Verify that ipa-cp correctly detects the dynamic type of an object
under destruction when doing devirtualization. */
/* { dg-do run } */
/* { dg-options "-O3 -fno-early-inlining -fno-inline" } */
extern "C" void abort (void);
class A
{
public:
int data;
~A();
virtual int foo (int i);
};
class B : public A
{
public:
virtual int foo (int i);
};
class C : public A
{
public:
virtual int foo (int i);
};
int A::foo (int i)
{
return i + 1;
}
int B::foo (int i)
{
return i + 2;
}
int C::foo (int i)
{
return i + 3;
}
static int middleman (class A *obj, int i)
{
return obj->foo (i);
}
int __attribute__ ((noinline,noclone)) get_input(void)
{
return 1;
}
A::~A ()
{
if (middleman (this, get_input ()) != 2)
abort ();
}
static void bah ()
{
class B b;
}
int main (int argc, char *argv[])
{
int i;
for (i = 0; i < 10; i++)
bah ();
return 0;
}
/* { dg-do run } */
extern "C" void abort ();
struct B *b;
struct B
{
virtual void f () { }
~B() { b->f(); }
};
struct D : public B
{
virtual void f () { abort (); }
};
int main ()
{
D d;
b = &d;
return 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