Commit 4bb6e7c8 authored by mythria's avatar mythria Committed by Commit bot

[Interpreter] Add support for keyed load / store ICs and named store IC to

bytecode graph builder

Adds implementation and tests for KeyedLoadIC, KeyedStoreIC and StoreIC to
bytecode graph builder.

BUG=v8:4280
LOG=N

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

Cr-Commit-Position: refs/heads/master@{#32116}
parent 6980f195
......@@ -460,89 +460,144 @@ void BytecodeGraphBuilder::VisitLoadICStrict(
}
void BytecodeGraphBuilder::VisitKeyedLoadICSloppy(
void BytecodeGraphBuilder::VisitLoadICSloppyWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildNamedLoad(iterator);
}
void BytecodeGraphBuilder::VisitKeyedLoadICStrict(
void BytecodeGraphBuilder::VisitLoadICStrictWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_strict(language_mode()));
BuildNamedLoad(iterator);
}
void BytecodeGraphBuilder::VisitLoadICSloppyWide(
void BytecodeGraphBuilder::BuildKeyedLoad(
const interpreter::BytecodeArrayIterator& iterator) {
Node* key = environment()->LookupAccumulator();
Node* object = environment()->LookupRegister(iterator.GetRegisterOperand(0));
VectorSlotPair feedback = CreateVectorSlotPair(iterator.GetIndexOperand(1));
const Operator* op = javascript()->LoadProperty(language_mode(), feedback);
Node* node = NewNode(op, object, key, BuildLoadFeedbackVector());
AddEmptyFrameStateInputs(node);
environment()->BindAccumulator(node);
}
void BytecodeGraphBuilder::VisitKeyedLoadICSloppy(
const interpreter::BytecodeArrayIterator& iterator) {
DCHECK(is_sloppy(language_mode()));
BuildNamedLoad(iterator);
BuildKeyedLoad(iterator);
}
void BytecodeGraphBuilder::VisitLoadICStrictWide(
void BytecodeGraphBuilder::VisitKeyedLoadICStrict(
const interpreter::BytecodeArrayIterator& iterator) {
DCHECK(is_strict(language_mode()));
BuildNamedLoad(iterator);
BuildKeyedLoad(iterator);
}
void BytecodeGraphBuilder::VisitKeyedLoadICSloppyWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildKeyedLoad(iterator);
}
void BytecodeGraphBuilder::VisitKeyedLoadICStrictWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_strict(language_mode()));
BuildKeyedLoad(iterator);
}
void BytecodeGraphBuilder::BuildNamedStore(
const interpreter::BytecodeArrayIterator& iterator) {
Node* value = environment()->LookupAccumulator();
Node* object = environment()->LookupRegister(iterator.GetRegisterOperand(0));
Handle<Name> name =
Handle<Name>::cast(iterator.GetConstantForIndexOperand(1));
VectorSlotPair feedback = CreateVectorSlotPair(iterator.GetIndexOperand(2));
const Operator* op =
javascript()->StoreNamed(language_mode(), name, feedback);
Node* node = NewNode(op, object, value, BuildLoadFeedbackVector());
AddEmptyFrameStateInputs(node);
environment()->BindAccumulator(value);
}
void BytecodeGraphBuilder::VisitStoreICSloppy(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildNamedStore(iterator);
}
void BytecodeGraphBuilder::VisitStoreICStrict(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_strict(language_mode()));
BuildNamedStore(iterator);
}
void BytecodeGraphBuilder::VisitKeyedStoreICSloppy(
void BytecodeGraphBuilder::VisitStoreICSloppyWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildNamedStore(iterator);
}
void BytecodeGraphBuilder::VisitKeyedStoreICStrict(
void BytecodeGraphBuilder::VisitStoreICStrictWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_strict(language_mode()));
BuildNamedStore(iterator);
}
void BytecodeGraphBuilder::VisitStoreICSloppyWide(
void BytecodeGraphBuilder::BuildKeyedStore(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
Node* value = environment()->LookupAccumulator();
Node* object = environment()->LookupRegister(iterator.GetRegisterOperand(0));
Node* key = environment()->LookupRegister(iterator.GetRegisterOperand(1));
VectorSlotPair feedback = CreateVectorSlotPair(iterator.GetIndexOperand(2));
const Operator* op = javascript()->StoreProperty(language_mode(), feedback);
Node* node = NewNode(op, object, key, value, BuildLoadFeedbackVector());
AddEmptyFrameStateInputs(node);
environment()->BindAccumulator(value);
}
void BytecodeGraphBuilder::VisitStoreICStrictWide(
void BytecodeGraphBuilder::VisitKeyedStoreICSloppy(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildKeyedStore(iterator);
}
void BytecodeGraphBuilder::VisitKeyedStoreICStrict(
const interpreter::BytecodeArrayIterator& iterator) {
DCHECK(is_strict(language_mode()));
BuildKeyedStore(iterator);
}
void BytecodeGraphBuilder::VisitKeyedStoreICSloppyWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_sloppy(language_mode()));
BuildKeyedStore(iterator);
}
void BytecodeGraphBuilder::VisitKeyedStoreICStrictWide(
const interpreter::BytecodeArrayIterator& iterator) {
UNIMPLEMENTED();
DCHECK(is_strict(language_mode()));
BuildKeyedStore(iterator);
}
......
......@@ -77,6 +77,11 @@ class BytecodeGraphBuilder {
return MakeNode(op, arraysize(buffer), buffer, false);
}
Node* NewNode(const Operator* op, Node* n1, Node* n2, Node* n3, Node* n4) {
Node* buffer[] = {n1, n2, n3, n4};
return MakeNode(op, arraysize(buffer), buffer, false);
}
Node* MakeNode(const Operator* op, int value_input_count, Node** value_inputs,
bool incomplete);
......@@ -94,6 +99,9 @@ class BytecodeGraphBuilder {
TypeofMode typeof_mode);
void BuildStoreGlobal(const interpreter::BytecodeArrayIterator& iterator);
void BuildNamedLoad(const interpreter::BytecodeArrayIterator& iterator);
void BuildKeyedLoad(const interpreter::BytecodeArrayIterator& iterator);
void BuildNamedStore(const interpreter::BytecodeArrayIterator& iterator);
void BuildKeyedStore(const interpreter::BytecodeArrayIterator& iterator);
void BuildCall(const interpreter::BytecodeArrayIterator& iterator);
void BuildBinaryOp(const Operator* op,
const interpreter::BytecodeArrayIterator& iterator);
......
......@@ -334,6 +334,172 @@ TEST(BytecodeGraphBuilderNamedLoad) {
}
TEST(BytecodeGraphBuilderKeyedLoad) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
Zone* zone = scope.main_zone();
Factory* factory = isolate->factory();
ExpectedSnippet<2> snippets[] = {
{"return p1[p2];",
{factory->NewNumberFromInt(10),
BytecodeGraphTester::NewObject("({val : 10})"),
factory->NewStringFromStaticChars("val")}},
{"return p1[100];",
{factory->NewStringFromStaticChars("abc"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(0)}},
{"var b = 100; return p1[b];",
{factory->NewStringFromStaticChars("abc"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(0)}},
{"'use strict'; return p1[p2];",
{factory->NewNumberFromInt(10),
BytecodeGraphTester::NewObject("({val : 10 })"),
factory->NewStringFromStaticChars("val")}},
{"'use strict'; return p1[100];",
{factory->NewNumberFromInt(10),
BytecodeGraphTester::NewObject("({100 : 10})"),
factory->NewNumberFromInt(0)}},
{"'use strict'; var b = p2; return p1[b];",
{factory->NewStringFromStaticChars("abc"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
{"var b;\n" REPEAT_127(SPACE, " b = p1[p2]; ") " return p1[p2];\n",
{factory->NewStringFromStaticChars("abc"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
{"'use strict'; var b;\n" REPEAT_127(SPACE,
" b = p1[p2]; ") "return p1[p2];\n",
{factory->NewStringFromStaticChars("abc"),
BytecodeGraphTester::NewObject("({ 100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
};
size_t num_snippets = sizeof(snippets) / sizeof(snippets[0]);
for (size_t i = 0; i < num_snippets; i++) {
ScopedVector<char> script(2048);
SNPrintF(script, "function %s(p1, p2) { %s };\n%s(0);", kFunctionName,
snippets[i].code_snippet, kFunctionName);
BytecodeGraphTester tester(isolate, zone, script.start());
auto callable = tester.GetCallable<Handle<Object>, Handle<Object>>();
Handle<Object> return_value =
callable(snippets[i].parameter(0), snippets[i].parameter(1))
.ToHandleChecked();
CHECK(return_value->SameValue(*snippets[i].return_value()));
}
}
TEST(BytecodeGraphBuilderNamedStore) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
Zone* zone = scope.main_zone();
Factory* factory = isolate->factory();
ExpectedSnippet<1> snippets[] = {
{"return p1.val = 20;",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({val : 10})")}},
{"p1.type = 'int'; return p1.type;",
{factory->NewStringFromStaticChars("int"),
BytecodeGraphTester::NewObject("({val : 10})")}},
{"p1.name = 'def'; return p1[\"name\"];",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({name : 'abc'})")}},
{"'use strict'; p1.val = 20; return p1.val;",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({val : 10 })")}},
{"'use strict'; return p1.type = 'int';",
{factory->NewStringFromStaticChars("int"),
BytecodeGraphTester::NewObject("({val : 10})")}},
{"'use strict'; p1.val = 20; return p1[\"val\"];",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({val : 10, name : 'abc'})")}},
{"var b = 'abc';\n" REPEAT_127(
SPACE, " p1.name = b; ") " p1.name = 'def'; return p1.name;\n",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({name : 'abc'})")}},
{"'use strict'; var b = 'def';\n" REPEAT_127(
SPACE, " p1.name = 'abc'; ") "p1.name = b; return p1.name;\n",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({ name : 'abc'})")}},
};
size_t num_snippets = sizeof(snippets) / sizeof(snippets[0]);
for (size_t i = 0; i < num_snippets; i++) {
ScopedVector<char> script(3072);
SNPrintF(script, "function %s(p1) { %s };\n%s({});", kFunctionName,
snippets[i].code_snippet, kFunctionName);
BytecodeGraphTester tester(isolate, zone, script.start());
auto callable = tester.GetCallable<Handle<Object>>();
Handle<Object> return_value =
callable(snippets[i].parameter(0)).ToHandleChecked();
CHECK(return_value->SameValue(*snippets[i].return_value()));
}
}
TEST(BytecodeGraphBuilderKeyedStore) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
Zone* zone = scope.main_zone();
Factory* factory = isolate->factory();
ExpectedSnippet<2> snippets[] = {
{"p1[p2] = 20; return p1[p2];",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({val : 10})"),
factory->NewStringFromStaticChars("val")}},
{"return p1[100] = 'def';",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(0)}},
{"var b = 100; p1[b] = 'def'; return p1[b];",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(0)}},
{"'use strict'; p1[p2] = 20; return p1[p2];",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({val : 10 })"),
factory->NewStringFromStaticChars("val")}},
{"'use strict'; return p1[100] = 20;",
{factory->NewNumberFromInt(20),
BytecodeGraphTester::NewObject("({100 : 10})"),
factory->NewNumberFromInt(0)}},
{"'use strict'; var b = p2; p1[b] = 'def'; return p1[b];",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
{"var b;\n" REPEAT_127(
SPACE, " b = p1[p2]; ") " p1[p2] = 'def'; return p1[p2];\n",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
{"'use strict'; var b;\n" REPEAT_127(
SPACE, " b = p1[p2]; ") " p1[p2] = 'def'; return p1[p2];\n",
{factory->NewStringFromStaticChars("def"),
BytecodeGraphTester::NewObject("({ 100 : 'abc'})"),
factory->NewNumberFromInt(100)}},
};
size_t num_snippets = sizeof(snippets) / sizeof(snippets[0]);
for (size_t i = 0; i < num_snippets; i++) {
ScopedVector<char> script(2048);
SNPrintF(script, "function %s(p1, p2) { %s };\n%s({});", kFunctionName,
snippets[i].code_snippet, kFunctionName);
BytecodeGraphTester tester(isolate, zone, script.start());
auto callable = tester.GetCallable<Handle<Object>>();
Handle<Object> return_value =
callable(snippets[i].parameter(0)).ToHandleChecked();
CHECK(return_value->SameValue(*snippets[i].return_value()));
}
}
TEST(BytecodeGraphBuilderPropertyCall) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
......
......@@ -365,9 +365,9 @@ TEST_F(BytecodeGraphBuilderTest, CallProperty0) {
size_t func_name_index = array_builder.GetConstantPoolEntry(func_name);
interpreter::Register reg0 = interpreter::Register(0);
array_builder.LoadNamedProperty(
array_builder.Parameter(1), func_name_index,
vector->GetIndex(load_slot), LanguageMode::SLOPPY)
array_builder.LoadNamedProperty(array_builder.Parameter(1), func_name_index,
vector->GetIndex(load_slot),
LanguageMode::SLOPPY)
.StoreAccumulatorInRegister(reg0)
.Call(reg0, array_builder.Parameter(1), 0, vector->GetIndex(call_slot))
.Return();
......@@ -408,9 +408,9 @@ TEST_F(BytecodeGraphBuilderTest, CallProperty2) {
interpreter::Register reg1 = interpreter::Register(1);
interpreter::Register reg2 = interpreter::Register(2);
interpreter::Register reg3 = interpreter::Register(3);
array_builder.LoadNamedProperty(
array_builder.Parameter(1), func_name_index,
vector->GetIndex(load_slot), LanguageMode::SLOPPY)
array_builder.LoadNamedProperty(array_builder.Parameter(1), func_name_index,
vector->GetIndex(load_slot),
LanguageMode::SLOPPY)
.StoreAccumulatorInRegister(reg0)
.LoadAccumulatorWithRegister(array_builder.Parameter(1))
.StoreAccumulatorInRegister(reg1)
......@@ -591,6 +591,136 @@ TEST_F(BytecodeGraphBuilderTest, Delete) {
}
}
TEST_F(BytecodeGraphBuilderTest, KeyedLoad) {
const int kValue = 100;
const bool kWideBytecode[] = {false, true};
TRACED_FOREACH(LanguageMode, language_mode, kLanguageModes) {
TRACED_FOREACH(bool, wide_bytecode, kWideBytecode) {
FeedbackVectorSpec feedback_spec(zone());
if (wide_bytecode) {
for (int i = 0; i < 128; i++) {
feedback_spec.AddLoadICSlot();
}
}
FeedbackVectorSlot slot = feedback_spec.AddLoadICSlot();
Handle<TypeFeedbackVector> vector =
NewTypeFeedbackVector(isolate(), &feedback_spec);
interpreter::BytecodeArrayBuilder array_builder(isolate(), zone());
array_builder.set_locals_count(1);
array_builder.set_context_count(0);
array_builder.set_parameter_count(2);
array_builder.LoadLiteral(Smi::FromInt(kValue))
.LoadKeyedProperty(array_builder.Parameter(1), vector->GetIndex(slot),
language_mode)
.Return();
Graph* graph = GetCompletedGraph(array_builder.ToBytecodeArray(), vector,
language_mode);
Node* ret = graph->end()->InputAt(0);
Node* start = graph->start();
Matcher<Node*> feedback_vector_matcher = IsFeedbackVector(start, start);
Matcher<Node*> load_keyed_matcher =
IsJSLoadProperty(IsParameter(1), IsNumberConstant(kValue),
feedback_vector_matcher, start, start);
EXPECT_THAT(ret, IsReturn(load_keyed_matcher, _, _));
}
}
}
TEST_F(BytecodeGraphBuilderTest, NamedStore) {
const int kValue = 100;
const bool kWideBytecode[] = {false, true};
TRACED_FOREACH(LanguageMode, language_mode, kLanguageModes) {
TRACED_FOREACH(bool, wide_bytecode, kWideBytecode) {
FeedbackVectorSpec feedback_spec(zone());
if (wide_bytecode) {
for (int i = 0; i < 128; i++) {
feedback_spec.AddLoadICSlot();
}
}
FeedbackVectorSlot slot = feedback_spec.AddLoadICSlot();
Handle<TypeFeedbackVector> vector =
NewTypeFeedbackVector(isolate(), &feedback_spec);
interpreter::BytecodeArrayBuilder array_builder(isolate(), zone());
array_builder.set_locals_count(1);
array_builder.set_context_count(0);
array_builder.set_parameter_count(2);
Handle<Name> name = GetName(isolate(), "val");
size_t name_index = array_builder.GetConstantPoolEntry(name);
array_builder.LoadLiteral(Smi::FromInt(kValue))
.StoreNamedProperty(array_builder.Parameter(1), name_index,
vector->GetIndex(slot), language_mode)
.Return();
Graph* graph = GetCompletedGraph(array_builder.ToBytecodeArray(), vector,
language_mode);
Node* ret = graph->end()->InputAt(0);
Node* start = graph->start();
Matcher<Node*> feedback_vector_matcher = IsFeedbackVector(start, start);
Matcher<Node*> store_named_matcher =
IsJSStoreNamed(name, IsParameter(1), IsNumberConstant(kValue),
feedback_vector_matcher, start, start);
EXPECT_THAT(ret, IsReturn(_, store_named_matcher, _));
}
}
}
TEST_F(BytecodeGraphBuilderTest, KeyedStore) {
const int kValue = 100;
const int kKey = 10;
const bool kWideBytecode[] = {false, true};
TRACED_FOREACH(LanguageMode, language_mode, kLanguageModes) {
TRACED_FOREACH(bool, wide_bytecode, kWideBytecode) {
FeedbackVectorSpec feedback_spec(zone());
if (wide_bytecode) {
for (int i = 0; i < 128; i++) {
feedback_spec.AddStoreICSlot();
}
}
FeedbackVectorSlot slot = feedback_spec.AddStoreICSlot();
Handle<TypeFeedbackVector> vector =
NewTypeFeedbackVector(isolate(), &feedback_spec);
interpreter::BytecodeArrayBuilder array_builder(isolate(), zone());
array_builder.set_locals_count(1);
array_builder.set_context_count(0);
array_builder.set_parameter_count(2);
array_builder.LoadLiteral(Smi::FromInt(kKey))
.StoreAccumulatorInRegister(interpreter::Register(0))
.LoadLiteral(Smi::FromInt(kValue))
.StoreKeyedProperty(array_builder.Parameter(1),
interpreter::Register(0), vector->GetIndex(slot),
language_mode)
.Return();
Graph* graph = GetCompletedGraph(array_builder.ToBytecodeArray(), vector,
language_mode);
Node* ret = graph->end()->InputAt(0);
Node* start = graph->start();
Matcher<Node*> feedback_vector_matcher = IsFeedbackVector(start, start);
Matcher<Node*> store_keyed_matcher = IsJSStoreProperty(
IsParameter(1), IsNumberConstant(kKey), IsNumberConstant(kValue),
feedback_vector_matcher, start, start);
EXPECT_THAT(ret, IsReturn(_, store_keyed_matcher, _));
}
}
}
} // namespace compiler
} // namespace internal
} // namespace v8
This diff is collapsed.
......@@ -358,11 +358,6 @@ Matcher<Node*> IsNumberToInt32(const Matcher<Node*>& input_matcher);
Matcher<Node*> IsNumberToUint32(const Matcher<Node*>& input_matcher);
Matcher<Node*> IsParameter(const Matcher<int> index_matcher);
Matcher<Node*> IsLoadFramePointer();
Matcher<Node*> IsJSLoadNamed(const Handle<Name> name,
const Matcher<Node*>& object_value_matcher,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSLoadGlobal(const Handle<Name> name,
const TypeofMode typeof_mode,
const Matcher<Node*>& feedback_vector_matcher,
......@@ -373,6 +368,28 @@ Matcher<Node*> IsJSStoreGlobal(const Handle<Name> name,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSLoadNamed(const Handle<Name> name,
const Matcher<Node*>& object_value_matcher,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSLoadProperty(const Matcher<Node*>& object_matcher,
const Matcher<Node*>& key_matcher,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSStoreNamed(const Handle<Name> name,
const Matcher<Node*>& object_matcher,
const Matcher<Node*>& value_matcher,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSStoreProperty(const Matcher<Node*>& object_matcher,
const Matcher<Node*>& key_matcher,
const Matcher<Node*>& value_matcher,
const Matcher<Node*>& feedback_vector_matcher,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
Matcher<Node*> IsJSCallFunction(std::vector<Matcher<Node*>> value_matchers,
const Matcher<Node*>& effect_matcher,
const Matcher<Node*>& control_matcher);
......
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