Commit 3db5773f by Ian Lance Taylor

escape: Implement assign phase.

    
    Implementation of the assign/connect phase.
    Statements containing expressions which alias local, parameter,
    and global objects are analyzed and the alias relationship between
    objects containing pointers are captured in a connection graph to
    summarize the assignments within a function.
    
    Reviewed-on: https://go-review.googlesource.com/18302

From-SVN: r237424
parent c8aa46d6
054ff1ece3dd5888a445efeaf3ae197b16d4186f
f768153eb2a7a72587c9c0997955cdbbc70322d0
The first line of this file holds the git revision number of the last
merge done from the gofrontend repository.
......@@ -88,6 +88,55 @@ Node::set_encoding(int enc)
}
bool
Node::is_big(Escape_context* context) const
{
Type* t = this->type();
if (t == NULL
|| t->is_call_multiple_result_type()
|| t->is_sink_type()
|| t->is_void_type()
|| t->is_abstract())
return false;
int64_t size;
bool ok = t->backend_type_size(context->gogo(), &size);
bool big = ok && (size < 0 || size > 10 * 1024 * 1024);
if (this->expr() != NULL)
{
if (this->expr()->allocation_expression() != NULL)
{
ok = t->deref()->backend_type_size(context->gogo(), &size);
big = big || size <= 0 || size >= (1 << 16);
}
else if (this->expr()->call_expression() != NULL)
{
Call_expression* call = this->expr()->call_expression();
Func_expression* fn = call->fn()->func_expression();
if (fn != NULL
&& fn->is_runtime_function()
&& (fn->runtime_code() == Runtime::MAKESLICE1
|| fn->runtime_code() == Runtime::MAKESLICE2
|| fn->runtime_code() == Runtime::MAKESLICE1BIG
|| fn->runtime_code() == Runtime::MAKESLICE2BIG))
{
// Second argument is length.
Expression_list::iterator p = call->args()->begin();
++p;
Numeric_constant nc;
unsigned long v;
if ((*p)->numeric_constant_value(&nc)
&& nc.to_unsigned_long(&v) == Numeric_constant::NC_UL_VALID)
big = big || v >= (1 << 16);
}
}
}
return big;
}
bool
Node::is_sink() const
{
if (this->object() != NULL
......@@ -161,6 +210,37 @@ Node::max_encoding(int e, int etype)
// Return a modified encoding for an input parameter that flows into an
// output parameter.
int
Node::note_inout_flows(int e, int index, Level level)
{
// Flow+level is encoded in two bits.
// 00 = not flow, xx = level+1 for 0 <= level <= maxEncodedLevel.
// 16 bits for Esc allows 6x2bits or 4x3bits or 3x4bits if additional
// information would be useful.
if (level.value() <= 0 && level.suffix_value() > 0)
return Node::max_encoding(e|ESCAPE_CONTENT_ESCAPES, Node::ESCAPE_NONE);
if (level.value() < 0)
return Node::ESCAPE_HEAP;
if (level.value() > ESCAPE_MAX_ENCODED_LEVEL)
level = Level::From(ESCAPE_MAX_ENCODED_LEVEL);
int encoded = level.value() + 1;
int shift = ESCAPE_BITS_PER_OUTPUT_IN_TAG * index + ESCAPE_RETURN_BITS;
int old = (e >> shift) & ESCAPE_BITS_MASK_FOR_TAG;
if (old == 0
|| (encoded != 0 && encoded < old))
old = encoded;
int encoded_flow = old << shift;
if (((encoded_flow >> shift) & ESCAPE_BITS_MASK_FOR_TAG) != old)
{
// Failed to encode. Put this on the heap.
return Node::ESCAPE_HEAP;
}
return (e & ~(ESCAPE_BITS_MASK_FOR_TAG << shift)) | encoded_flow;
}
// Class Escape_context.
Escape_context::Escape_context(Gogo* gogo, bool recursive)
......@@ -493,14 +573,1258 @@ Gogo::discover_analysis_sets()
this->traverse(&ead);
}
// Traverse all label and goto statements and mark the underlying label
// as looping or not looping.
class Escape_analysis_loop : public Traverse
{
public:
Escape_analysis_loop()
: Traverse(traverse_statements)
{ }
int
statement(Block*, size_t*, Statement*);
};
int
Escape_analysis_loop::statement(Block*, size_t*, Statement* s)
{
if (s->label_statement() != NULL)
s->label_statement()->label()->set_nonlooping();
else if (s->goto_statement() != NULL)
{
if (s->goto_statement()->label()->nonlooping())
s->goto_statement()->label()->set_looping();
}
return TRAVERSE_CONTINUE;
}
// Traversal class used to look at all interesting statements within a function
// in order to build a connectivity graph between all nodes within a context's
// scope.
class Escape_analysis_assign : public Traverse
{
public:
Escape_analysis_assign(Escape_context* context, Named_object* fn)
: Traverse(traverse_statements
| traverse_expressions),
context_(context), fn_(fn)
{ }
// Model statements within a function as assignments and flows between nodes.
int
statement(Block*, size_t*, Statement*);
// Model expressions within a function as assignments and flows between nodes.
int
expression(Expression**);
// Model calls within a function as assignments and flows between arguments
// and results.
void
call(Call_expression* call);
// Model the assignment of DST to SRC.
void
assign(Node* dst, Node* src);
// Model the assignment of DST to dereference of SRC.
void
assign_deref(Node* dst, Node* src);
// Model the input-to-output assignment flow of one of a function call's
// arguments, where the flow is encoding in NOTE.
int
assign_from_note(std::string* note, const std::vector<Node*>& dsts,
Node* src);
// Record the flow of SRC to DST in DST.
void
flows(Node* dst, Node* src);
private:
// The escape context for this set of functions.
Escape_context* context_;
// The current function being analyzed.
Named_object* fn_;
};
// Model statements within a function as assignments and flows between nodes.
int
Escape_analysis_assign::statement(Block*, size_t*, Statement* s)
{
// Adjust the loop depth as we enter/exit blocks related to for statements.
bool is_for_statement = (s->is_block_statement()
&& s->block_statement()->is_lowered_for_statement());
if (is_for_statement)
this->context_->increase_loop_depth();
s->traverse_contents(this);
if (is_for_statement)
this->context_->decrease_loop_depth();
switch (s->classification())
{
case Statement::STATEMENT_VARIABLE_DECLARATION:
{
Named_object* var = s->variable_declaration_statement()->var();
Node* var_node = Node::make_node(var);
Node::Escape_state* state = var_node->state(this->context_, NULL);
state->loop_depth = this->context_->loop_depth();
// Set the loop depth for this declaration.
if (var->is_variable()
&& var->var_value()->init() != NULL)
{
Node* init_node = Node::make_node(var->var_value()->init());
this->assign(var_node, init_node);
}
}
break;
case Statement::STATEMENT_LABEL:
{
if (s->label_statement()->label()->looping())
this->context_->increase_loop_depth();
}
break;
case Statement::STATEMENT_SWITCH:
case Statement::STATEMENT_TYPE_SWITCH:
// Want to model the assignment of each case variable to the switched upon
// variable. This should be lowered into assignment statements; nothing
// to here if that's the case.
// TODO(cmang): Verify.
break;
case Statement::STATEMENT_ASSIGNMENT:
{
Assignment_statement* assn = s->assignment_statement();
Node* lhs = Node::make_node(assn->lhs());
Node* rhs = Node::make_node(assn->rhs());
// TODO(cmang): Add special case for escape analysis no-op:
// func (b *Buffer) Foo() {
// n, m := ...
// b.buf = b.buf[n:m]
// }
// This is okay for now, it just means b escapes; it is conservative.
this->assign(lhs, rhs);
}
break;
case Statement::STATEMENT_SEND:
{
Node* sent_node = Node::make_node(s->send_statement()->val());
this->assign(this->context_->sink(), sent_node);
}
break;
case Statement::STATEMENT_DEFER:
if (this->context_->loop_depth() == 1)
break;
// fallthrough
case Statement::STATEMENT_GO:
{
// Defer f(x) or go f(x).
// Both f and x escape to the heap.
Thunk_statement* thunk = s->thunk_statement();
if (thunk->call()->call_expression() == NULL)
break;
Call_expression* call = thunk->call()->call_expression();
Node* func_node = Node::make_node(call->fn());
this->assign(this->context_->sink(), func_node);
if (call->args() != NULL)
{
for (Expression_list::const_iterator p = call->args()->begin();
p != call->args()->end();
++p)
{
Node* arg_node = Node::make_node(*p);
this->assign(this->context_->sink(), arg_node);
}
}
}
break;
// TODO(cmang): Associate returned values with dummy return nodes.
default:
break;
}
return TRAVERSE_SKIP_COMPONENTS;
}
// Model expressions within a function as assignments and flows between nodes.
int
Escape_analysis_assign::expression(Expression** pexpr)
{
// Big stuff escapes unconditionally.
Node* n = Node::make_node(*pexpr);
if ((n->encoding() & ESCAPE_MASK) != int(Node::ESCAPE_HEAP)
&& n->is_big(this->context_))
{
n->set_encoding(Node::ESCAPE_HEAP);
(*pexpr)->address_taken(true);
this->assign(this->context_->sink(), n);
}
if ((*pexpr)->func_expression() == NULL)
(*pexpr)->traverse_subexpressions(this);
switch ((*pexpr)->classification())
{
case Expression::EXPRESSION_CALL:
{
Call_expression* call = (*pexpr)->call_expression();
this->call(call);
Func_expression* fe = call->fn()->func_expression();
if (fe != NULL && fe->is_runtime_function())
{
switch (fe->runtime_code())
{
case Runtime::PANIC:
{
// Argument could leak through recover.
Node* panic_arg = Node::make_node(call->args()->front());
this->assign(this->context_->sink(), panic_arg);
}
break;
case Runtime::APPEND:
{
// Unlike gc/esc.go, a call to append has already had its
// varargs lowered into a slice of arguments.
// The content of the appended slice leaks.
Node* appended = Node::make_node(call->args()->back());
this->assign_deref(this->context_->sink(), appended);
// The content of the original slice leaks as well.
Node* appendee = Node::make_node(call->args()->back());
this->assign_deref(this->context_->sink(), appendee);
}
break;
case Runtime::COPY:
{
// Lose track of the copied content.
Node* copied = Node::make_node(call->args()->back());
this->assign_deref(this->context_->sink(), copied);
}
break;
case Runtime::MAKECHAN:
case Runtime::MAKECHANBIG:
case Runtime::MAKEMAP:
case Runtime::MAKEMAPBIG:
case Runtime::MAKESLICE1:
case Runtime::MAKESLICE2:
case Runtime::MAKESLICE1BIG:
case Runtime::MAKESLICE2BIG:
case Runtime::BYTE_ARRAY_TO_STRING:
case Runtime::INT_ARRAY_TO_STRING:
case Runtime::STRING_TO_BYTE_ARRAY:
case Runtime::STRING_TO_INT_ARRAY:
case Runtime::STRING_PLUS:
case Runtime::CONSTRUCT_MAP:
case Runtime::INT_TO_STRING:
{
Node* runtime_node = Node::make_node(fe);
this->context_->track(runtime_node);
}
break;
default:
break;
}
}
}
break;
case Expression::EXPRESSION_ALLOCATION:
{
// Same as above; this is Runtime::NEW.
Node* alloc_node = Node::make_node(*pexpr);
this->context_->track(alloc_node);
}
break;
case Expression::EXPRESSION_CONVERSION:
{
Type_conversion_expression* tce = (*pexpr)->conversion_expression();
Node* tce_node = Node::make_node(tce);
Node* converted = Node::make_node(tce->expr());
this->context_->track(tce_node);
this->assign(tce_node, converted);
}
break;
case Expression::EXPRESSION_FIXED_ARRAY_CONSTRUCTION:
case Expression::EXPRESSION_SLICE_CONSTRUCTION:
{
Node* array_node = Node::make_node(*pexpr);
if ((*pexpr)->slice_literal() != NULL)
this->context_->track(array_node);
Expression_list* vals = ((*pexpr)->slice_literal() != NULL
? (*pexpr)->slice_literal()->vals()
: (*pexpr)->array_literal()->vals());
if (vals != NULL)
{
// Connect the array to its values.
for (Expression_list::const_iterator p = vals->begin();
p != vals->end();
++p)
if ((*p) != NULL)
this->assign(array_node, Node::make_node(*p));
}
}
break;
case Expression::EXPRESSION_STRUCT_CONSTRUCTION:
{
Node* struct_node = Node::make_node(*pexpr);
Expression_list* vals = (*pexpr)->struct_literal()->vals();
if (vals != NULL)
{
// Connect the struct to its values.
for (Expression_list::const_iterator p = vals->begin();
p != vals->end();
++p)
{
if ((*p) != NULL)
this->assign(struct_node, Node::make_node(*p));
}
}
}
break;
case Expression::EXPRESSION_HEAP:
{
Node* pointer_node = Node::make_node(*pexpr);
Node* lit_node = Node::make_node((*pexpr)->heap_expression()->expr());
this->context_->track(pointer_node);
// Connect pointer node to literal node; if the pointer node escapes, so
// does the literal node.
this->assign(pointer_node, lit_node);
}
break;
case Expression::EXPRESSION_BOUND_METHOD:
{
Node* bound_node = Node::make_node(*pexpr);
this->context_->track(bound_node);
Expression* obj = (*pexpr)->bound_method_expression()->first_argument();
Node* obj_node = Node::make_node(obj);
// A bound method implies the receiver will be used outside of the
// lifetime of the method in some way. We lose track of the receiver.
this->assign(this->context_->sink(), obj_node);
}
break;
case Expression::EXPRESSION_MAP_CONSTRUCTION:
{
Map_construction_expression* mce = (*pexpr)->map_literal();
Node* map_node = Node::make_node(mce);
this->context_->track(map_node);
// All keys and values escape to memory.
if (mce->vals() != NULL)
{
for (Expression_list::const_iterator p = mce->vals()->begin();
p != mce->vals()->end();
++p)
{
if ((*p) != NULL)
this->assign(this->context_->sink(), Node::make_node(*p));
}
}
}
break;
case Expression::EXPRESSION_FUNC_REFERENCE:
{
Func_expression* fe = (*pexpr)->func_expression();
if (fe->closure() != NULL)
{
// Connect captured variables to the closure.
Node* closure_node = Node::make_node(fe);
this->context_->track(closure_node);
// A closure expression already exists as the heap expression:
// &struct{f func_code, v []*Variable}{...}.
// Link closure to the addresses of the variables enclosed.
Heap_expression* he = fe->closure()->heap_expression();
Struct_construction_expression* sce = he->expr()->struct_literal();
// First field is function code, other fields are variable
// references.
Expression_list::const_iterator p = sce->vals()->begin();
++p;
for (; p != sce->vals()->end(); ++p)
{
Node* enclosed_node = Node::make_node(*p);
Node::Escape_state* state =
enclosed_node->state(this->context_, NULL);
state->loop_depth = this->context_->loop_depth();
this->assign(closure_node, enclosed_node);
}
}
}
break;
case Expression::EXPRESSION_UNARY:
{
if ((*pexpr)->unary_expression()->op() != OPERATOR_AND)
break;
Node* addr_node = Node::make_node(*pexpr);
this->context_->track(addr_node);
Expression* operand = (*pexpr)->unary_expression()->operand();
Named_object* var = NULL;
if (operand->var_expression() != NULL)
var = operand->var_expression()->named_object();
else if (operand->enclosed_var_expression() != NULL)
var = operand->enclosed_var_expression()->variable();
else if (operand->temporary_reference_expression() != NULL)
{
// Found in runtime/chanbarrier_test.go. The address of a struct
// reference is usually a heap expression, except when it is a part
// of a case statement. In that case, it is lowered into a
// temporary reference and never linked to the heap expression that
// initializes it. In general, when taking the address of some
// temporary, the analysis should really be looking at the initial
// value of that temporary.
Temporary_reference_expression* tre =
operand->temporary_reference_expression();
if (tre->statement() != NULL
&& tre->statement()->temporary_statement()->init() != NULL)
{
Expression* init =
tre->statement()->temporary_statement()->init();
Node* init_node = Node::make_node(init);
this->assign(addr_node, init_node);
}
}
if (var == NULL)
break;
if (var->is_variable()
&& !var->var_value()->is_parameter())
{
// For &x, use the loop depth of x if known.
Node::Escape_state* addr_state =
addr_node->state(this->context_, NULL);
Node* operand_node = Node::make_node(operand);
Node::Escape_state* operand_state =
operand_node->state(this->context_, NULL);
if (operand_state->loop_depth != 0)
addr_state->loop_depth = operand_state->loop_depth;
}
else if ((var->is_variable()
&& var->var_value()->is_parameter())
|| var->is_result_variable())
{
Node::Escape_state* addr_state =
addr_node->state(this->context_, NULL);
addr_state->loop_depth = 1;
}
}
break;
default:
break;
}
return TRAVERSE_SKIP_COMPONENTS;
}
// Model calls within a function as assignments and flows between arguments
// and results.
void
Escape_analysis_assign::call(Call_expression* call)
{
Func_expression* fn = call->fn()->func_expression();
Function_type* fntype = call->get_function_type();
bool indirect = false;
// Interface method calls or closure calls are indirect calls.
if (fntype == NULL
|| (fntype->is_method()
&& fntype->receiver()->type()->interface_type() != NULL)
|| fn == NULL
|| (fn->named_object()->is_function()
&& fn->named_object()->func_value()->enclosing() != NULL))
indirect = true;
Node* call_node = Node::make_node(call);
std::vector<Node*> arg_nodes;
if (call->fn()->interface_field_reference_expression() != NULL)
{
Interface_field_reference_expression* ifre =
call->fn()->interface_field_reference_expression();
Node* field_node = Node::make_node(ifre->expr());
arg_nodes.push_back(field_node);
}
if (call->args() != NULL)
{
for (Expression_list::const_iterator p = call->args()->begin();
p != call->args()->end();
++p)
arg_nodes.push_back(Node::make_node(*p));
}
if (indirect)
{
// We don't know what happens to the parameters through indirect calls.
// Be conservative and assume they all flow to theSink.
for (std::vector<Node*>::iterator p = arg_nodes.begin();
p != arg_nodes.end();
++p)
{
this->assign(this->context_->sink(), *p);
}
this->context_->init_retvals(call_node, fntype);
return;
}
// If FN is an untagged function.
if (fn != NULL
&& fn->named_object()->is_function()
&& !fntype->is_tagged())
{
Function* f = fn->named_object()->func_value();
const Bindings* callee_bindings = f->block()->bindings();
const Typed_identifier_list* results = fntype->results();
if (results != NULL)
{
// Setup output list on this call node.
Node::Escape_state* state = call_node->state(this->context_, NULL);
for (Typed_identifier_list::const_iterator p1 = results->begin();
p1 != results->end();
++p1)
{
if (p1->name().empty() || Gogo::is_sink_name(p1->name()))
continue;
Named_object* result_no =
callee_bindings->lookup_local(p1->name());
go_assert(result_no != NULL);
Node* result_node = Node::make_node(result_no);
state->retvals.push_back(result_node);
}
}
std::vector<Node*>::iterator p = arg_nodes.begin();
if (fntype->is_method()
&& fntype->receiver()->type()->has_pointer())
{
std::string rcvr_name = fntype->receiver()->name();
if (rcvr_name.empty() || Gogo::is_sink_name(rcvr_name))
;
else
{
Named_object* rcvr_no =
callee_bindings->lookup_local(fntype->receiver()->name());
go_assert(rcvr_no != NULL);
Node* rcvr_node = Node::make_node(rcvr_no);
this->assign(rcvr_node, *p);
}
++p;
}
const Typed_identifier_list* til = fntype->parameters();
if (til != NULL)
{
for (Typed_identifier_list::const_iterator p1 = til->begin();
p1 != til->end();
++p1, ++p)
{
if (p1->name().empty() || Gogo::is_sink_name(p1->name()))
continue;
Named_object* param_no =
callee_bindings->lookup_local(p1->name());
go_assert(param_no != NULL);
Expression* arg = (*p)->expr();
if (arg->var_expression() != NULL
&& arg->var_expression()->named_object() == param_no)
continue;
Node* param_node = Node::make_node(param_no);
this->assign(param_node, *p);
}
for (; p != arg_nodes.end(); ++p)
{
this->assign(this->context_->sink(), *p);
}
}
return;
}
Node::Escape_state* call_state = call_node->state(this->context_, NULL);
this->context_->init_retvals(call_node, fntype);
// Receiver.
std::vector<Node*>::iterator p = arg_nodes.begin();
if (fntype->is_method()
&& fntype->receiver()->type()->has_pointer()
&& p != arg_nodes.end())
{
// First argument to call will be the receiver.
std::string* note = fntype->receiver()->note();
if (fntype->receiver()->type()->points_to() == NULL
&& (*p)->expr()->unary_expression() != NULL
&& (*p)->expr()->unary_expression()->op() == OPERATOR_AND)
{
// This is a call to a value method that has been lowered into a call
// to a pointer method. Gccgo generates a pointer method for all
// method calls and takes the address of the value passed as the
// receiver then immediately dereferences it within the function.
// In this case, the receiver does not escape.
}
else
{
if (!Type::are_identical(fntype->receiver()->type(),
(*p)->expr()->type(), true, NULL))
{
// This will be converted later, preemptively track it instead
// of its conversion expression which will show up in a later pass.
this->context_->track(*p);
}
this->assign_from_note(note, call_state->retvals, *p);
}
p++;
}
const Typed_identifier_list* til = fntype->parameters();
if (til != NULL)
{
for (Typed_identifier_list::const_iterator pn = til->begin();
pn != til->end() && p != arg_nodes.end();
++pn, ++p)
{
if (!Type::are_identical(pn->type(), (*p)->expr()->type(),
true, NULL))
{
// This will be converted later, preemptively track it instead
// of its conversion expression which will show up in a later pass.
this->context_->track(*p);
}
// TODO(cmang): Special care for varargs parameter?
Type* t = pn->type();
if (t != NULL
&& t->has_pointer())
{
std::string* note = pn->note();
int enc = this->assign_from_note(note, call_state->retvals, *p);
if (enc == Node::ESCAPE_NONE
&& (call->is_deferred()
|| call->is_concurrent()))
{
// TODO(cmang): Mark the argument as strictly non-escaping.
}
}
}
for (; p != arg_nodes.end(); ++p)
{
this->assign(this->context_->sink(), *p);
}
}
}
// Model the assignment of DST to SRC.
// Assert that SRC somehow gets assigned to DST.
// DST might need to be examined for evaluations that happen inside of it.
// For example, in [DST]*f(x) = [SRC]y, we lose track of the indirection and
// DST becomes the sink in our model.
void
Escape_analysis_assign::assign(Node* dst, Node* src)
{
if (dst->expr() != NULL)
{
// Analyze the lhs of the assignment.
// Replace DST with this->context_->sink() if we can't track it.
Expression* e = dst->expr();
switch (e->classification())
{
case Expression::EXPRESSION_VAR_REFERENCE:
{
// If DST is a global variable, we have no way to track it.
Named_object* var = e->var_expression()->named_object();
if (var->is_variable() && var->var_value()->is_global())
dst = this->context_->sink();
}
break;
case Expression::EXPRESSION_FIELD_REFERENCE:
{
Expression* strct = e->field_reference_expression()->expr();
if (strct->heap_expression() != NULL)
{
// When accessing the field of a struct reference, we lose
// track of the indirection.
dst = this->context_->sink();
break;
}
// Treat DST.x = SRC as if it were DST = SRC.
Node* struct_node = Node::make_node(strct);
this->assign(struct_node, src);
return;
}
case Expression::EXPRESSION_ARRAY_INDEX:
{
Array_index_expression* are = e->array_index_expression();
if (!are->array()->type()->is_slice_type())
{
// Treat DST[i] = SRC as if it were DST = SRC if DST if a fixed
// array.
Node* array_node = Node::make_node(are->array());
this->assign(array_node, src);
return;
}
// Lose track of the slice dereference.
dst = this->context_->sink();
}
break;
case Expression::EXPRESSION_UNARY:
// Lose track of the dereference.
if (e->unary_expression()->op() == OPERATOR_MULT)
dst = this->context_->sink();
break;
case Expression::EXPRESSION_MAP_INDEX:
{
// Lose track of the map's key and value.
Expression* index = e->map_index_expression()->index();
Node* index_node = Node::make_node(index);
this->assign(this->context_->sink(), index_node);
dst = this->context_->sink();
}
break;
default:
// TODO(cmang): Add debugging info here: only a few expressions
// should leave DST unmodified.
break;
}
}
if (src->expr() != NULL)
{
Expression* e = src->expr();
switch (e->classification())
{
case Expression::EXPRESSION_VAR_REFERENCE:
// DST = var
case Expression::EXPRESSION_HEAP:
// DST = &T{...}.
case Expression::EXPRESSION_FIXED_ARRAY_CONSTRUCTION:
case Expression::EXPRESSION_SLICE_CONSTRUCTION:
// DST = [...]T{...}.
case Expression::EXPRESSION_MAP_CONSTRUCTION:
// DST = map[T]V{...}.
case Expression::EXPRESSION_STRUCT_CONSTRUCTION:
// DST = T{...}.
case Expression::EXPRESSION_ALLOCATION:
// DST = new(T).
case Expression::EXPRESSION_BOUND_METHOD:
// DST = x.M.
this->flows(dst, src);
break;
case Expression::EXPRESSION_UNSAFE_CONVERSION:
{
Expression* underlying = e->unsafe_conversion_expression()->expr();
Node* underlying_node = Node::make_node(underlying);
this->assign(dst, underlying_node);
}
break;
case Expression::EXPRESSION_ENCLOSED_VAR_REFERENCE:
{
Named_object* var = e->enclosed_var_expression()->variable();
Node* var_node = Node::make_node(var);
this->flows(dst, var_node);
}
break;
case Expression::EXPRESSION_CALL:
{
Call_expression* call = e->call_expression();
Func_expression* fe = call->fn()->func_expression();
if (fe != NULL && fe->is_runtime_function())
{
switch (fe->runtime_code())
{
case Runtime::APPEND:
{
// Append returns the first argument.
// The subsequent arguments are already leaked because
// they are operands to append.
Node* appendee = Node::make_node(call->args()->front());
this->assign(dst, appendee);
break;
}
case Runtime::MAKECHAN:
case Runtime::MAKECHANBIG:
case Runtime::MAKEMAP:
case Runtime::MAKEMAPBIG:
case Runtime::MAKESLICE1:
case Runtime::MAKESLICE2:
case Runtime::MAKESLICE1BIG:
case Runtime::MAKESLICE2BIG:
// DST = make(...).
case Runtime::BYTE_ARRAY_TO_STRING:
// DST = string([]byte{...}).
case Runtime::INT_ARRAY_TO_STRING:
// DST = string([]int{...}).
case Runtime::STRING_TO_BYTE_ARRAY:
// DST = []byte(str).
case Runtime::STRING_TO_INT_ARRAY:
// DST = []int(str).
case Runtime::STRING_PLUS:
// DST = str1 + str2
case Runtime::CONSTRUCT_MAP:
// When building a map literal's backend representation.
// Likely never seen here and covered in
// Expression::EXPRESSION_MAP_CONSTRUCTION.
case Runtime::INT_TO_STRING:
// DST = string(i).
case Runtime::IFACEE2E2:
case Runtime::IFACEI2E2:
case Runtime::IFACEE2I2:
case Runtime::IFACEI2I2:
case Runtime::IFACEE2T2P:
case Runtime::IFACEI2T2P:
case Runtime::IFACEE2T2:
case Runtime::IFACEI2T2:
case Runtime::CONVERT_INTERFACE:
// All versions of interface conversion that might result
// from a type assertion. Some of these are the result of
// a tuple type assertion statement and may not be covered
// by the case in Expression::EXPRESSION_CONVERSION or
// Expression::EXPRESSION_TYPE_GUARD.
this->flows(dst, src);
break;
default:
break;
}
break;
}
else if (fe != NULL
&& fe->named_object()->is_function()
&& fe->named_object()->func_value()->is_method()
&& (call->is_deferred()
|| call->is_concurrent()))
{
// For a method call thunk, lose track of the call and treat it
// as if DST = RECEIVER.
Node* rcvr_node = Node::make_node(call->args()->front());
this->assign(dst, rcvr_node);
break;
}
// TODO(cmang): Handle case from issue 4529.
// Node* call_node = Node::make_node(e);
// Node::Escape_state* call_state = call_node->state(this->context_, NULL);
// std::vector<Node*> retvals = call_state->retvals;
// for (std::vector<Node*>::const_iterator p = retvals.begin();
// p != retvals.end();
// ++p)
// this->flows(dst, *p);
}
break;
case Expression::EXPRESSION_FUNC_REFERENCE:
if (e->func_expression()->closure() != NULL)
{
// If SRC is a reference to a function closure, DST flows into
// the underyling closure variable.
Expression* closure = e->func_expression()->closure();
Node* closure_node = Node::make_node(closure);
this->flows(dst, closure_node);
}
break;
case Expression::EXPRESSION_FIELD_REFERENCE:
{
// A non-pointer can't escape from a struct.
if (!e->type()->has_pointer())
break;
}
case Expression::EXPRESSION_CONVERSION:
case Expression::EXPRESSION_TYPE_GUARD:
case Expression::EXPRESSION_ARRAY_INDEX:
case Expression::EXPRESSION_STRING_INDEX:
{
Expression* left = NULL;
if (e->field_reference_expression() != NULL)
{
left = e->field_reference_expression()->expr();
if (left->unary_expression() != NULL
&& left->unary_expression()->op() == OPERATOR_MULT)
{
// DST = (*x).f
this->flows(dst, src);
break;
}
}
else if (e->conversion_expression() != NULL)
left = e->conversion_expression()->expr();
else if (e->type_guard_expression() != NULL)
left = e->type_guard_expression()->expr();
else if (e->array_index_expression() != NULL)
{
Array_index_expression* aie = e->array_index_expression();
if (e->type()->is_slice_type())
left = aie->array();
else if (!aie->array()->type()->is_slice_type())
{
// Indexing an array preserves the input value.
Node* array_node = Node::make_node(aie->array());
this->assign(dst, array_node);
break;
}
else
{
this->flows(dst, src);
break;
}
}
else if (e->string_index_expression() != NULL)
{
String_index_expression* sie = e->string_index_expression();
if (e->type()->is_slice_type())
left = sie->string();
else if (!sie->string()->type()->is_slice_type())
{
// Indexing a string preserves the input value.
Node* string_node = Node::make_node(sie->string());
this->assign(dst, string_node);
break;
}
else
{
this->flows(dst, src);
break;
}
}
go_assert(left != NULL);
// Conversions, field access, and slicing all preserve the input
// value.
Node* left_node = Node::make_node(left);
this->assign(dst, left_node);
}
break;
case Expression::EXPRESSION_BINARY:
{
switch (e->binary_expression()->op())
{
case OPERATOR_PLUS:
case OPERATOR_MINUS:
case OPERATOR_XOR:
case OPERATOR_MULT:
case OPERATOR_DIV:
case OPERATOR_MOD:
case OPERATOR_LSHIFT:
case OPERATOR_RSHIFT:
case OPERATOR_AND:
case OPERATOR_BITCLEAR:
{
Node* left = Node::make_node(e->binary_expression()->left());
this->assign(dst, left);
Node* right = Node::make_node(e->binary_expression()->right());
this->assign(dst, right);
}
break;
default:
break;
}
}
break;
case Expression::EXPRESSION_UNARY:
{
switch (e->unary_expression()->op())
{
case OPERATOR_PLUS:
case OPERATOR_MINUS:
case OPERATOR_XOR:
{
Node* op_node =
Node::make_node(e->unary_expression()->operand());
this->assign(dst, op_node);
}
break;
case OPERATOR_MULT:
// DST = *x
case OPERATOR_AND:
// DST = &x
this->flows(dst, src);
break;
default:
break;
}
}
break;
case Expression::EXPRESSION_TEMPORARY_REFERENCE:
{
Statement* temp = e->temporary_reference_expression()->statement();
if (temp != NULL
&& temp->temporary_statement()->init() != NULL)
{
Expression* init = temp->temporary_statement()->init();
Node* init_node = Node::make_node(init);
this->assign(dst, init_node);
}
}
break;
default:
// TODO(cmang): Add debug info here; this should not be reachable.
// For now, just to be conservative, we'll just say dst flows to src.
break;
}
}
}
// Model the assignment of DST to an indirection of SRC.
void
Escape_analysis_assign::assign_deref(Node* dst, Node* src)
{
if (src->expr() != NULL)
{
switch (src->expr()->classification())
{
case Expression::EXPRESSION_BOOLEAN:
case Expression::EXPRESSION_STRING:
case Expression::EXPRESSION_INTEGER:
case Expression::EXPRESSION_FLOAT:
case Expression::EXPRESSION_COMPLEX:
case Expression::EXPRESSION_NIL:
case Expression::EXPRESSION_IOTA:
// No need to try indirections on literal values
// or numeric constants.
return;
default:
break;
}
}
this->assign(dst, this->context_->add_dereference(src));
}
// Model the input-to-output assignment flow of one of a function call's
// arguments, where the flow is encoded in NOTE.
int
Escape_analysis_assign::assign_from_note(std::string* note,
const std::vector<Node*>& dsts,
Node* src)
{
int enc = Escape_note::parse_tag(note);
if (src->expr() != NULL)
{
switch (src->expr()->classification())
{
case Expression::EXPRESSION_BOOLEAN:
case Expression::EXPRESSION_STRING:
case Expression::EXPRESSION_INTEGER:
case Expression::EXPRESSION_FLOAT:
case Expression::EXPRESSION_COMPLEX:
case Expression::EXPRESSION_NIL:
case Expression::EXPRESSION_IOTA:
// There probably isn't a note for a literal value. Literal values
// usually don't escape unless we lost track of the value some how.
return enc;
default:
break;
}
}
if (enc == Node::ESCAPE_UNKNOWN)
{
// Lost track of the value.
this->assign(this->context_->sink(), src);
return enc;
}
else if (enc == Node::ESCAPE_NONE)
return enc;
// If the content inside a parameter (reached via indirection) escapes to
// the heap, mark it.
if ((enc & ESCAPE_CONTENT_ESCAPES) != 0)
this->assign(this->context_->sink(), this->context_->add_dereference(src));
int save_enc = enc;
enc >>= ESCAPE_RETURN_BITS;
for (std::vector<Node*>::const_iterator p = dsts.begin();
enc != 0 && p != dsts.end();
++p)
{
// Prefer the lowest-level path to the reference (for escape purposes).
// Two-bit encoding (for example. 1, 3, and 4 bits are other options)
// 01 = 0-level
// 10 = 1-level, (content escapes),
// 11 = 2-level, (content of content escapes).
int bits = enc & ESCAPE_BITS_MASK_FOR_TAG;
if (bits > 0)
{
Node* n = src;
for (int i = 0; i < bits - 1; ++i)
{
// Encode level > 0 as indirections.
n = this->context_->add_dereference(n);
}
this->assign(*p, n);
}
enc >>= ESCAPE_BITS_PER_OUTPUT_IN_TAG;
}
// If there are too many outputs to fit in the tag, that is handled on the
// encoding end as ESCAPE_HEAP, so there is no need to check here.
return save_enc;
}
// Record the flow of SRC to DST in DST.
void
Escape_analysis_assign::flows(Node* dst, Node* src)
{
// Don't bother capturing the flow from scalars.
if (src->expr() != NULL
&& !src->expr()->type()->has_pointer())
return;
// Don't confuse a blank identifier with the sink.
if (dst->is_sink() && dst != this->context_->sink())
return;
Node::Escape_state* dst_state = dst->state(this->context_, NULL);
Node::Escape_state* src_state = src->state(this->context_, NULL);
if (dst == src
|| dst_state == src_state
|| dst_state->flows.find(src) != dst_state->flows.end()
|| src_state->flows.find(dst) != src_state->flows.end())
return;
if (dst_state->flows.empty())
this->context_->add_dst(dst);
dst_state->flows.insert(src);
}
// Build a connectivity graph between nodes in the function being analyzed.
void
Gogo::assign_connectivity(Escape_context*, Named_object*)
Gogo::assign_connectivity(Escape_context* context, Named_object* fn)
{
// TODO(cmang): Model the flow analysis of input parameters and results for a
// function.
// TODO(cmang): Analyze the current function's body.
// Must be defined outside of this package.
if (!fn->is_function())
return;
int save_depth = context->loop_depth();
context->set_loop_depth(1);
Escape_analysis_assign ea(context, fn);
Function::Results* res = fn->func_value()->result_variables();
if (res != NULL)
{
for (Function::Results::const_iterator p = res->begin();
p != res->end();
++p)
{
Node* res_node = Node::make_node(*p);
Node::Escape_state* res_state = res_node->state(context, fn);
res_state->loop_depth = 0;
// If this set of functions is recursive, we lose track of the return values.
// Just say that the result flows to the sink.
if (context->recursive())
ea.flows(context->sink(), res_node);
}
}
const Bindings* callee_bindings = fn->func_value()->block()->bindings();
Function_type* fntype = fn->func_value()->type();
Typed_identifier_list* params = (fntype->parameters() != NULL
? fntype->parameters()->copy()
: new Typed_identifier_list);
if (fntype->receiver() != NULL)
params->push_back(*fntype->receiver());
for (Typed_identifier_list::const_iterator p = params->begin();
p != params->end();
++p)
{
if (p->name().empty() || Gogo::is_sink_name(p->name()))
continue;
Named_object* param_no = callee_bindings->lookup_local(p->name());
go_assert(param_no != NULL);
Node* param_node = Node::make_node(param_no);
Node::Escape_state* param_state = param_node->state(context, fn);
param_state->loop_depth = 1;
if (!p->type()->has_pointer())
continue;
// External function? Parameters must escape unless //go:noescape is set.
// TODO(cmang): Implement //go:noescape directive.
if (fn->package() != NULL)
param_node->set_encoding(Node::ESCAPE_HEAP);
else
param_node->set_encoding(Node::ESCAPE_NONE);
// TODO(cmang): Track this node in no_escape list.
}
Escape_analysis_loop el;
fn->func_value()->traverse(&el);
fn->func_value()->traverse(&ea);
context->set_loop_depth(save_depth);
}
// Propagate escape information across the nodes modeled in this Analysis_set.
......
......@@ -206,7 +206,9 @@ class Node
void
set_encoding(int enc);
// Is this node a sink?
bool
is_big(Escape_context*) const;
bool
is_sink() const;
......@@ -250,6 +252,11 @@ class Node
static int
max_encoding(int e, int etype);
// Return a modified encoding for an input parameter that flows into an
// output parameter.
static int
note_inout_flows(int e, int index, Level level);
private:
// The classification of this Node.
Node_classification classification_;
......
......@@ -2578,7 +2578,7 @@ class Label
public:
Label(const std::string& name)
: name_(name), location_(Linemap::unknown_location()), snapshot_(NULL),
refs_(), is_used_(false), blabel_(NULL)
refs_(), is_used_(false), blabel_(NULL), depth_(DEPTH_UNKNOWN)
{ }
// Return the label's name.
......@@ -2601,6 +2601,26 @@ class Label
set_is_used()
{ this->is_used_ = true; }
// Return whether this label is looping.
bool
looping() const
{ return this->depth_ == DEPTH_LOOPING; }
// Set this label as looping.
void
set_looping()
{ this->depth_ = DEPTH_LOOPING; }
// Return whether this label is nonlooping.
bool
nonlooping() const
{ return this->depth_ == DEPTH_NONLOOPING; }
// Set this label as nonlooping.
void
set_nonlooping()
{ this->depth_ = DEPTH_NONLOOPING; }
// Return the location of the definition.
Location
location() const
......@@ -2660,6 +2680,16 @@ class Label
is_dummy_label() const
{ return this->name_ == "_"; }
// A classification of a label's looping depth.
enum Loop_depth
{
DEPTH_UNKNOWN,
// A label never jumped to.
DEPTH_NONLOOPING,
// A label jumped to.
DEPTH_LOOPING
};
private:
// The name of the label.
std::string name_;
......@@ -2675,6 +2705,8 @@ class Label
bool is_used_;
// The backend representation.
Blabel* blabel_;
// The looping depth of this label, for escape analysis.
Loop_depth depth_;
};
// An unnamed label. These are used when lowering loops.
......
......@@ -1793,40 +1793,6 @@ Statement::make_statement(Expression* expr, bool is_ignored)
return new Expression_statement(expr, is_ignored);
}
// A block statement--a list of statements which may include variable
// definitions.
class Block_statement : public Statement
{
public:
Block_statement(Block* block, Location location)
: Statement(STATEMENT_BLOCK, location),
block_(block)
{ }
protected:
int
do_traverse(Traverse* traverse)
{ return this->block_->traverse(traverse); }
void
do_determine_types()
{ this->block_->determine_types(); }
bool
do_may_fall_through() const
{ return this->block_->may_fall_through(); }
Bstatement*
do_get_backend(Translate_context* context);
void
do_dump_statement(Ast_dump_context*) const;
private:
Block* block_;
};
// Convert a block to the backend representation of a statement.
Bstatement*
......@@ -2944,37 +2910,13 @@ Statement::make_continue_statement(Unnamed_label* label,
return new Bc_statement(false, label, location);
}
// A goto statement.
// Class Goto_statement.
class Goto_statement : public Statement
int
Goto_statement::do_traverse(Traverse*)
{
public:
Goto_statement(Label* label, Location location)
: Statement(STATEMENT_GOTO, location),
label_(label)
{ }
protected:
int
do_traverse(Traverse*)
{ return TRAVERSE_CONTINUE; }
void
do_check_types(Gogo*);
bool
do_may_fall_through() const
{ return false; }
Bstatement*
do_get_backend(Translate_context*);
void
do_dump_statement(Ast_dump_context*) const;
private:
Label* label_;
};
return TRAVERSE_CONTINUE;
}
// Check types for a label. There aren't any types per se, but we use
// this to give an error if the label was never defined.
......@@ -3016,35 +2958,21 @@ Statement::make_goto_statement(Label* label, Location location)
return new Goto_statement(label, location);
}
// A goto statement to an unnamed label.
// Class Goto_unnamed_statement.
class Goto_unnamed_statement : public Statement
int
Goto_unnamed_statement::do_traverse(Traverse*)
{
public:
Goto_unnamed_statement(Unnamed_label* label, Location location)
: Statement(STATEMENT_GOTO_UNNAMED, location),
label_(label)
{ }
protected:
int
do_traverse(Traverse*)
{ return TRAVERSE_CONTINUE; }
bool
do_may_fall_through() const
{ return false; }
Bstatement*
do_get_backend(Translate_context* context)
{ return this->label_->get_goto(context, this->location()); }
return TRAVERSE_CONTINUE;
}
void
do_dump_statement(Ast_dump_context*) const;
// Convert the goto unnamed statement to the backend representation.
private:
Unnamed_label* label_;
};
Bstatement*
Goto_unnamed_statement::do_get_backend(Translate_context* context)
{
return this->label_->get_goto(context, this->location());
}
// Dump the AST representation for an unnamed goto statement
......@@ -3109,32 +3037,27 @@ Statement::make_label_statement(Label* label, Location location)
return new Label_statement(label, location);
}
// An unnamed label statement.
// Class Unnamed_label_statement.
class Unnamed_label_statement : public Statement
{
public:
Unnamed_label_statement(Unnamed_label* label)
: Statement(STATEMENT_UNNAMED_LABEL, label->location()),
label_(label)
{ }
Unnamed_label_statement::Unnamed_label_statement(Unnamed_label* label)
: Statement(STATEMENT_UNNAMED_LABEL, label->location()),
label_(label)
{ }
protected:
int
do_traverse(Traverse*)
{ return TRAVERSE_CONTINUE; }
int
Unnamed_label_statement::do_traverse(Traverse*)
{
return TRAVERSE_CONTINUE;
}
Bstatement*
do_get_backend(Translate_context* context)
{ return this->label_->get_definition(context); }
// Get the backend definition for this unnamed label statement.
void
do_dump_statement(Ast_dump_context*) const;
Bstatement*
Unnamed_label_statement::do_get_backend(Translate_context* context)
{
return this->label_->get_definition(context);
}
private:
// The label.
Unnamed_label* label_;
};
// Dump the AST representation for an unnamed label definition statement.
......@@ -5209,7 +5132,9 @@ For_statement::do_lower(Gogo*, Named_object*, Block* enclosing,
b->set_end_location(end_loc);
return Statement::make_block_statement(b, loc);
Statement* bs = Statement::make_block_statement(b, loc);
bs->block_statement()->set_is_lowered_for_statement();
return bs;
}
// Return the break label, creating it if necessary.
......
......@@ -19,9 +19,13 @@ class Assignment_statement;
class Temporary_statement;
class Variable_declaration_statement;
class Expression_statement;
class Block_statement;
class Return_statement;
class Thunk_statement;
class Goto_statement;
class Goto_unnamed_statement;
class Label_statement;
class Unnamed_label_statement;
class If_statement;
class For_statement;
class For_range_statement;
......@@ -366,6 +370,12 @@ class Statement
return this->convert<Expression_statement, STATEMENT_EXPRESSION>();
}
// If this is an block statement, return it. Otherwise return
// NULL.
Block_statement*
block_statement()
{ return this->convert<Block_statement, STATEMENT_BLOCK>(); }
// If this is a return statement, return it. Otherwise return NULL.
Return_statement*
return_statement()
......@@ -376,11 +386,26 @@ class Statement
Thunk_statement*
thunk_statement();
// If this is a goto statement, return it. Otherwise return NULL.
Goto_statement*
goto_statement()
{ return this->convert<Goto_statement, STATEMENT_GOTO>(); }
// If this is a goto_unnamed statement, return it. Otherwise return NULL.
Goto_unnamed_statement*
goto_unnamed_statement()
{ return this->convert<Goto_unnamed_statement, STATEMENT_GOTO_UNNAMED>(); }
// If this is a label statement, return it. Otherwise return NULL.
Label_statement*
label_statement()
{ return this->convert<Label_statement, STATEMENT_LABEL>(); }
// If this is an unnamed_label statement, return it. Otherwise return NULL.
Unnamed_label_statement*
unnamed_label_statement()
{ return this->convert<Unnamed_label_statement, STATEMENT_UNNAMED_LABEL>(); }
// If this is an if statement, return it. Otherwise return NULL.
If_statement*
if_statement()
......@@ -762,6 +787,50 @@ class Expression_statement : public Statement
bool is_ignored_;
};
// A block statement--a list of statements which may include variable
// definitions.
class Block_statement : public Statement
{
public:
Block_statement(Block* block, Location location)
: Statement(STATEMENT_BLOCK, location),
block_(block), is_lowered_for_statement_(false)
{ }
void
set_is_lowered_for_statement()
{ this->is_lowered_for_statement_ = true; }
bool
is_lowered_for_statement()
{ return this->is_lowered_for_statement_; }
protected:
int
do_traverse(Traverse* traverse)
{ return this->block_->traverse(traverse); }
void
do_determine_types()
{ this->block_->determine_types(); }
bool
do_may_fall_through() const
{ return this->block_->may_fall_through(); }
Bstatement*
do_get_backend(Translate_context* context);
void
do_dump_statement(Ast_dump_context*) const;
private:
Block* block_;
// True if this block statement represents a lowered for statement.
bool is_lowered_for_statement_;
};
// A send statement.
class Send_statement : public Statement
......@@ -1162,6 +1231,74 @@ class Defer_statement : public Thunk_statement
do_dump_statement(Ast_dump_context*) const;
};
// A goto statement.
class Goto_statement : public Statement
{
public:
Goto_statement(Label* label, Location location)
: Statement(STATEMENT_GOTO, location),
label_(label)
{ }
// Return the label being jumped to.
Label*
label() const
{ return this->label_; }
protected:
int
do_traverse(Traverse*);
void
do_check_types(Gogo*);
bool
do_may_fall_through() const
{ return false; }
Bstatement*
do_get_backend(Translate_context*);
void
do_dump_statement(Ast_dump_context*) const;
private:
Label* label_;
};
// A goto statement to an unnamed label.
class Goto_unnamed_statement : public Statement
{
public:
Goto_unnamed_statement(Unnamed_label* label, Location location)
: Statement(STATEMENT_GOTO_UNNAMED, location),
label_(label)
{ }
Unnamed_label*
unnamed_label() const
{ return this->label_; }
protected:
int
do_traverse(Traverse*);
bool
do_may_fall_through() const
{ return false; }
Bstatement*
do_get_backend(Translate_context* context);
void
do_dump_statement(Ast_dump_context*) const;
private:
Unnamed_label* label_;
};
// A label statement.
class Label_statement : public Statement
......@@ -1173,7 +1310,7 @@ class Label_statement : public Statement
{ }
// Return the label itself.
const Label*
Label*
label() const
{ return this->label_; }
......@@ -1192,6 +1329,28 @@ class Label_statement : public Statement
Label* label_;
};
// An unnamed label statement.
class Unnamed_label_statement : public Statement
{
public:
Unnamed_label_statement(Unnamed_label* label);
protected:
int
do_traverse(Traverse*);
Bstatement*
do_get_backend(Translate_context* context);
void
do_dump_statement(Ast_dump_context*) const;
private:
// The label.
Unnamed_label* label_;
};
// An if statement.
class If_statement : public Statement
......
......@@ -4502,10 +4502,7 @@ class Call_multiple_result_type : public Type
protected:
bool
do_has_pointer() const
{
go_assert(saw_errors());
return false;
}
{ return false; }
bool
do_compare_is_identity(Gogo*)
......
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