blob: 6dc9388eb78cb26247a8b8de76aa30444668f5f2 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "vm/compiler/backend/inliner.h"
#include "vm/compiler/backend/il.h"
#include "vm/compiler/backend/il_printer.h"
#include "vm/compiler/backend/il_test_helper.h"
#include "vm/compiler/compiler_pass.h"
#include "vm/debugger_api_impl_test.h"
#include "vm/object.h"
#include "vm/unit_test.h"
namespace dart {
// Test that the redefinition for an inlined polymorphic function used with
// multiple receiver cids does not have a concrete type.
ISOLATE_UNIT_TEST_CASE(Inliner_PolyInliningRedefinition) {
const char* kScript = R"(
abstract class A {
String toInline() { return "A"; }
}
class B extends A {}
class C extends A {
@override
String toInline() { return "C";}
}
class D extends A {}
testInlining(A arg) {
arg.toInline();
}
main() {
for (var i = 0; i < 10; i++) {
testInlining(B());
testInlining(C());
testInlining(D());
}
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function =
Function::Handle(GetFunction(root_library, "testInlining"));
Invoke(root_library, "main");
TestPipeline pipeline(function, CompilerPass::kJIT);
FlowGraph* flow_graph = pipeline.RunPasses({
CompilerPass::kComputeSSA,
CompilerPass::kApplyICData,
CompilerPass::kTryOptimizePatterns,
CompilerPass::kSetOuterInliningId,
CompilerPass::kTypePropagation,
CompilerPass::kApplyClassIds,
CompilerPass::kInlining,
});
auto entry = flow_graph->graph_entry()->normal_entry();
EXPECT(entry != nullptr);
EXPECT(entry->initial_definitions()->length() == 1);
EXPECT(entry->initial_definitions()->At(0)->IsParameter());
ParameterInstr* param = entry->initial_definitions()->At(0)->AsParameter();
// First we find the start of the prelude for the inlined instruction,
// and also keep a reference to the LoadClassId instruction for later.
LoadClassIdInstr* lcid = nullptr;
BranchInstr* prelude = nullptr;
ILMatcher cursor(flow_graph, entry);
RELEASE_ASSERT(cursor.TryMatch(
{
{kMatchLoadClassId, &lcid},
{kMatchBranch, &prelude},
},
/*insert_before=*/kMoveGlob));
const Class& cls = Class::Handle(
root_library.LookupClass(String::Handle(Symbols::New(thread, "B"))));
Definition* cid_B = flow_graph->GetConstant(Smi::Handle(Smi::New(cls.id())));
Instruction* current = prelude;
// We walk false branches until we either reach a branch instruction that uses
// B's cid for comparison to the value returned from the LCID instruction
// above, or a default case if there was no branch instruction for B's cid.
while (true) {
EXPECT(current->IsBranch());
const ComparisonInstr* check = current->AsBranch()->comparison();
EXPECT(check->left()->definition() == lcid);
if (check->right()->definition() == cid_B) break;
current = current->SuccessorAt(1);
// By following false paths, we should be walking a series of blocks that
// looks like:
// B#[target]:#
// Branch if <check on class ID>
// If we end up not finding a branch, then we're in a default case
// that contains a class check.
current = current->next();
if (!current->IsBranch()) {
break;
}
}
// If we found a branch that checks against the class ID, we follow the true
// branch to a block that contains only a goto to the desired join block.
if (current->IsBranch()) {
current = current->SuccessorAt(0);
} else {
// We're in the default case, which will check the class ID to make sure
// it's the one expected for the fallthrough. That check will be followed
// by a goto to the desired join block.
EXPECT(current->IsRedefinition());
const auto redef = current->AsRedefinition();
EXPECT(redef->value()->definition() == lcid);
current = current->next();
EXPECT(current->IsCheckClassId());
EXPECT(current->AsCheckClassId()->value()->definition() == redef);
}
current = current->next();
EXPECT(current->IsGoto());
current = current->AsGoto()->successor();
// Now we should be at a block that starts like:
// BY[join]:# pred(...)
// vW <- Redefinition(vV)
//
// where vV is a reference to the function parameter (the receiver of
// the inlined function).
current = current->next();
EXPECT(current->IsRedefinition());
EXPECT(current->AsRedefinition()->value()->definition() == param);
EXPECT(current->AsRedefinition()->Type()->ToCid() == kDynamicCid);
}
ISOLATE_UNIT_TEST_CASE(Inliner_TypedData_Regress7551) {
const char* kScript = R"(
import 'dart:typed_data';
setValue(Int32List list, int value) {
list[0] = value;
}
main() {
final list = Int32List(10);
setValue(list, 0x1122334455);
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function =
Function::Handle(GetFunction(root_library, "setValue"));
Invoke(root_library, "main");
TestPipeline pipeline(function, CompilerPass::kJIT);
FlowGraph* flow_graph = pipeline.RunPasses({
CompilerPass::kComputeSSA,
CompilerPass::kApplyICData,
CompilerPass::kTryOptimizePatterns,
CompilerPass::kSetOuterInliningId,
CompilerPass::kTypePropagation,
CompilerPass::kApplyClassIds,
CompilerPass::kInlining,
});
auto entry = flow_graph->graph_entry()->normal_entry();
EXPECT(entry->initial_definitions()->length() == 2);
EXPECT(entry->initial_definitions()->At(0)->IsParameter());
EXPECT(entry->initial_definitions()->At(1)->IsParameter());
ParameterInstr* list_param =
entry->initial_definitions()->At(0)->AsParameter();
ParameterInstr* value_param =
entry->initial_definitions()->At(1)->AsParameter();
ILMatcher cursor(flow_graph, entry);
CheckArrayBoundInstr* bounds_check_instr = nullptr;
UnboxInt32Instr* unbox_instr = nullptr;
StoreIndexedInstr* store_instr = nullptr;
RELEASE_ASSERT(cursor.TryMatch({
{kMoveGlob},
{kMatchAndMoveCheckArrayBound, &bounds_check_instr},
{kMatchAndMoveUnboxInt32, &unbox_instr},
{kMatchAndMoveStoreIndexed, &store_instr},
}));
RELEASE_ASSERT(unbox_instr->InputAt(0)->definition()->OriginalDefinition() ==
value_param);
RELEASE_ASSERT(store_instr->InputAt(0)->definition() == list_param);
RELEASE_ASSERT(store_instr->InputAt(2)->definition() == unbox_instr);
RELEASE_ASSERT(unbox_instr->is_truncating());
}
#if defined(DART_PRECOMPILER)
// Verifies that all calls are inlined in List.generate call
// with a simple closure.
ISOLATE_UNIT_TEST_CASE(Inliner_List_generate) {
const char* kScript = R"(
foo(n) => List<int>.generate(n, (int x) => x, growable: false);
main() {
foo(100);
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function = Function::Handle(GetFunction(root_library, "foo"));
TestPipeline pipeline(function, CompilerPass::kAOT);
FlowGraph* flow_graph = pipeline.RunPasses({});
auto entry = flow_graph->graph_entry()->normal_entry();
ILMatcher cursor(flow_graph, entry, /*trace=*/true,
ParallelMovesHandling::kSkip);
RELEASE_ASSERT(cursor.TryMatch({
kMoveGlob,
kMatchAndMoveCreateArray,
kMatchAndMoveUnboxInt64,
kMatchAndMoveGoto,
// Loop header
kMatchAndMoveJoinEntry,
kMatchAndMoveCheckStackOverflow,
kMatchAndMoveBranchTrue,
// Loop body
kMatchAndMoveTargetEntry,
kMatchAndMoveBoxInt64,
kMatchAndMoveStoreIndexed,
kMatchAndMoveBinaryInt64Op,
kMatchAndMoveGoto,
// Loop header once again
kMatchAndMoveJoinEntry,
kMatchAndMoveCheckStackOverflow,
kMatchAndMoveBranchFalse,
// After loop
kMatchAndMoveTargetEntry,
kMatchReturn,
}));
}
// Verifies that pragma-decorated call gets inlined.
ISOLATE_UNIT_TEST_CASE(Inliner_always_consider_inlining) {
const char* kScript = R"(
choice() {
dynamic x;
return x == 123;
}
@pragma("vm:always-consider-inlining")
bar(baz) {
if (baz is String) {
return 1;
}
if (baz is num) {
return 2;
}
if (baz is bool) {
dynamic j = 0;
for (int i = 0; i < 1024; i++) {
j += "i: $i".length;
}
return j;
}
return 4;
}
bbar(bbaz, something) {
if (bbaz == null) {
return "null";
}
return bar(bbaz);
}
main(args) {
print(bbar(42, "something"));
print(bbar(choice() ? "abc": 42, "something"));
print(bbar("abc", "something"));
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function = Function::Handle(GetFunction(root_library, "main"));
TestPipeline pipeline(function, CompilerPass::kAOT);
FlowGraph* flow_graph = pipeline.RunPasses({});
auto entry = flow_graph->graph_entry()->normal_entry();
ILMatcher cursor(flow_graph, entry, /*trace=*/true);
StaticCallInstr* call_print1;
StaticCallInstr* call_print2;
StaticCallInstr* call_print3;
StaticCallInstr* call_bar;
RELEASE_ASSERT(cursor.TryMatch({
kMoveGlob,
{kMatchAndMoveStaticCall, &call_print1},
kMoveGlob,
{kMatchAndMoveStaticCall, &call_bar},
kMoveGlob,
{kMatchAndMoveStaticCall, &call_print2},
kMoveGlob,
{kMatchAndMoveStaticCall, &call_print3},
kMoveGlob,
kMatchReturn,
}));
EXPECT(strcmp(call_print1->function().UserVisibleNameCString(), "print") ==
0);
EXPECT(strcmp(call_print2->function().UserVisibleNameCString(), "print") ==
0);
EXPECT(strcmp(call_print3->function().UserVisibleNameCString(), "print") ==
0);
EXPECT(strcmp(call_bar->function().UserVisibleNameCString(), "bar") == 0);
}
// Verifies that List.of gets inlined.
ISOLATE_UNIT_TEST_CASE(Inliner_List_of_inlined) {
const char* kScript = R"(
main() {
final foo = List<String>.filled(100, "bar");
final the_copy1 = List.of(foo, growable: false);
print('${the_copy1.length}');
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function = Function::Handle(GetFunction(root_library, "main"));
TestPipeline pipeline(function, CompilerPass::kAOT);
FlowGraph* flow_graph = pipeline.RunPasses({});
auto entry = flow_graph->graph_entry()->normal_entry();
ILMatcher cursor(flow_graph, entry, /*trace=*/true);
StaticCallInstr* call_print;
RELEASE_ASSERT(cursor.TryMatch({
kMoveGlob,
kMatchAndMoveJoinEntry,
kMoveGlob,
kMatchAndMoveBranchTrue,
kMoveGlob,
kMatchAndMoveJoinEntry,
kMatchAndMoveCheckStackOverflow,
kMatchAndMoveBranchFalse,
kMatchAndMoveTargetEntry,
kMoveGlob,
kMatchAndMoveJoinEntry,
kMoveGlob,
kMatchAndMoveBranchFalse,
kMoveGlob,
{kMatchAndMoveStaticCall, &call_print},
kMoveGlob,
kMatchReturn,
}));
EXPECT(strcmp(call_print->function().UserVisibleNameCString(), "print") == 0);
// Length is fully forwarded and string interpolation is constant folded.
EXPECT_PROPERTY(call_print->ArgumentAt(0),
it.IsConstant() && it.AsConstant()->value().IsString() &&
String::Cast(it.AsConstant()->value()).Equals("100"));
}
#endif // defined(DART_PRECOMPILER)
// Test that when force-optimized functions get inlined, deopt_id and
// environment for instructions coming from those functions get overwritten
// by deopt_id and environment from the call itself.
ISOLATE_UNIT_TEST_CASE(Inliner_InlineForceOptimized) {
const char* kScript = R"(
import 'dart:ffi';
@pragma('vm:never-inline')
int foo(int x) {
dynamic ptr = Pointer.fromAddress(x);
return x + ptr.hashCode;
}
main() {
int r = 0;
for (int i = 0; i < 1000; i++) {
r += foo(r);
}
return r;
}
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& function = Function::Handle(GetFunction(root_library, "foo"));
Invoke(root_library, "main");
TestPipeline pipeline(function, CompilerPass::kJIT);
FlowGraph* flow_graph = pipeline.RunPasses({
CompilerPass::kComputeSSA,
CompilerPass::kApplyICData,
CompilerPass::kTryOptimizePatterns,
CompilerPass::kSetOuterInliningId,
CompilerPass::kTypePropagation,
CompilerPass::kApplyClassIds,
});
auto entry = flow_graph->graph_entry()->normal_entry();
StaticCallInstr* call_instr = nullptr;
{
ILMatcher cursor(flow_graph, entry);
RELEASE_ASSERT(cursor.TryMatch({
{kMoveGlob},
{kMatchAndMoveStaticCall, &call_instr},
}));
EXPECT(strcmp(call_instr->function().UserVisibleNameCString(),
"Pointer.fromAddress") == 0);
}
pipeline.RunAdditionalPasses({
CompilerPass::kInlining,
});
AllocateObjectInstr* allocate_object_instr = nullptr;
{
ILMatcher cursor(flow_graph, entry);
RELEASE_ASSERT(cursor.TryMatch({
{kMoveGlob},
{kMatchAndMoveAllocateObject, &allocate_object_instr},
}));
}
// Ensure that AllocateObject instruction that came from force-optimized
// function has deopt and environment taken from the Call instruction.
EXPECT(call_instr->deopt_id() == allocate_object_instr->deopt_id());
EXPECT(DeoptId::IsDeoptBefore(call_instr->deopt_id()));
auto allocate_object_instr_env = allocate_object_instr->env();
EXPECT(allocate_object_instr_env->LazyDeoptToBeforeDeoptId());
EXPECT(allocate_object_instr_env->Outermost()->GetDeoptId() ==
call_instr->deopt_id());
const auto call_instr_env = call_instr->env();
const intptr_t call_first_index =
call_instr_env->Length() - call_instr->InputCount();
const intptr_t allocate_first_index =
allocate_object_instr_env->Length() - call_instr->InputCount();
for (intptr_t i = 0; i < call_instr->InputCount(); i++) {
EXPECT(call_instr_env->ValueAt(call_first_index + i)->definition() ==
allocate_object_instr_env->ValueAt(allocate_first_index + i)
->definition());
}
}
static void TestPrint(Dart_NativeArguments args) {
Dart_EnterScope();
Dart_Handle handle = Dart_GetNativeArgument(args, 0);
const char* str = nullptr;
Dart_StringToCString(handle, &str);
OS::Print("%s\n", str);
Dart_ExitScope();
}
void InspectStack(Dart_NativeArguments args) {
#ifndef PRODUCT
Dart_EnterScope();
Dart_StackTrace stacktrace;
Dart_Handle result = Dart_GetStackTrace(&stacktrace);
EXPECT_VALID(result);
intptr_t frame_count = 0;
result = Dart_StackTraceLength(stacktrace, &frame_count);
EXPECT_VALID(result);
EXPECT_EQ(3, frame_count);
// Test something bigger than the preallocated size to verify nothing was
// truncated.
EXPECT(102 > StackTrace::kPreallocatedStackdepth);
Dart_Handle function_name;
Dart_Handle script_url;
intptr_t line_number = 0;
intptr_t column_number = 0;
const char* cstr = "";
const char* test_lib = "file:///test-lib";
// Top frame is InspectStack().
Dart_ActivationFrame frame;
result = Dart_GetActivationFrame(stacktrace, 0, &frame);
EXPECT_VALID(result);
result = Dart_ActivationFrameInfo(frame, &function_name, &script_url,
&line_number, &column_number);
EXPECT_VALID(result);
Dart_StringToCString(function_name, &cstr);
EXPECT_STREQ("InspectStack", cstr);
Dart_StringToCString(script_url, &cstr);
EXPECT_STREQ(test_lib, cstr);
EXPECT_EQ(11, line_number);
EXPECT_EQ(24, column_number);
// Second frame is foo() positioned at call to InspectStack().
result = Dart_GetActivationFrame(stacktrace, 1, &frame);
EXPECT_VALID(result);
result = Dart_ActivationFrameInfo(frame, &function_name, &script_url,
&line_number, &column_number);
EXPECT_VALID(result);
Dart_StringToCString(function_name, &cstr);
EXPECT_STREQ("foo", cstr);
Dart_StringToCString(script_url, &cstr);
EXPECT_STREQ(test_lib, cstr);
// Bottom frame positioned at main().
result = Dart_GetActivationFrame(stacktrace, frame_count - 1, &frame);
EXPECT_VALID(result);
result = Dart_ActivationFrameInfo(frame, &function_name, &script_url,
&line_number, &column_number);
EXPECT_VALID(result);
Dart_StringToCString(function_name, &cstr);
EXPECT_STREQ("main", cstr);
Dart_StringToCString(script_url, &cstr);
EXPECT_STREQ(test_lib, cstr);
// Out-of-bounds frames.
result = Dart_GetActivationFrame(stacktrace, frame_count, &frame);
EXPECT(Dart_IsError(result));
result = Dart_GetActivationFrame(stacktrace, -1, &frame);
EXPECT(Dart_IsError(result));
Dart_SetReturnValue(args, Dart_NewInteger(42));
Dart_ExitScope();
#endif // !PRODUCT
}
static Dart_NativeFunction PrintAndInspectResolver(Dart_Handle name,
int argument_count,
bool* auto_setup_scope) {
ASSERT(auto_setup_scope != nullptr);
*auto_setup_scope = true;
const char* cstr = nullptr;
Dart_Handle result = Dart_StringToCString(name, &cstr);
EXPECT_VALID(result);
if (strcmp(cstr, "testPrint") == 0) {
return &TestPrint;
} else {
return &InspectStack;
}
}
TEST_CASE(Inliner_InlineAndRunForceOptimized) {
auto check_handle = [](Dart_Handle handle) {
if (Dart_IsError(handle)) {
OS::PrintErr("Encountered unexpected error: %s\n", Dart_GetError(handle));
FATAL("Aborting");
}
return handle;
};
auto get_integer = [&](Dart_Handle lib, const char* name) {
Dart_Handle handle = Dart_GetField(lib, check_handle(NewString(name)));
check_handle(handle);
int64_t value = 0;
handle = Dart_IntegerToInt64(handle, &value);
check_handle(handle);
OS::Print("Field '%s': %" Pd64 "\n", name, value);
return value;
};
const char* kScriptChars = R"(
import 'dart:ffi';
import 'dart:_internal';
int _add(int a, int b) => a + b;
@pragma('vm:external-name', "testPrint")
external void print(String s);
@pragma("vm:external-name", "InspectStack")
external InspectStack();
@pragma('vm:never-inline')
void nop() {}
int prologueCount = 0;
int epilogueCount = 0;
@pragma('vm:never-inline')
void countPrologue() {
prologueCount++;
print('countPrologue: $prologueCount');
}
@pragma('vm:never-inline')
void countEpilogue() {
epilogueCount++;
print('countEpilogue: $epilogueCount');
}
@pragma('vm:force-optimize')
@pragma('vm:idempotent')
@pragma('vm:prefer-inline')
int idempotentForceOptimizedFunction(int x) {
countPrologue();
print('deoptimizing');
InspectStack();
VMInternalsForTesting.deoptimizeFunctionsOnStack();
InspectStack();
print('deoptimizing after');
countEpilogue();
return _add(x, 1);
}
@pragma('vm:never-inline')
int foo(int x, {bool onlyOptimizeFunction = false}) {
if (onlyOptimizeFunction) {
print('Optimizing `foo`');
for (int i = 0; i < 100000; i++) nop();
}
print('Running `foo`');
return x + idempotentForceOptimizedFunction(x+1) + 1;
}
void main() {
for (int i = 0; i < 100; i++) {
print('\n\nround=$i');
// Get the `foo` function optimized while leaving prologue/epilogue counters
// untouched.
final (a, b) = (prologueCount, epilogueCount);
foo(i, onlyOptimizeFunction: true);
prologueCount = a;
epilogueCount = b;
// Execute the optimized function `foo` function (which has the
// `idempotentForceOptimizedFunction` inlined).
final result = foo(i);
if (result != (2 * i + 3)) throw 'Expected ${2 * i + 3} but got $result!';
}
}
)";
DisableBackgroundCompilationScope scope;
Dart_Handle lib =
check_handle(TestCase::LoadTestScript(kScriptChars, nullptr));
Dart_SetNativeResolver(lib, &PrintAndInspectResolver, nullptr);
// We disable OSR to ensure we control when the function gets optimized,
// namely afer a call to `foo(..., onlyOptimizeFunction:true)` it will be
// optimized and during a call to `foo(...)` it will get deoptimized.
IsolateGroup::Current()->set_use_osr(false);
// Run the test.
check_handle(Dart_Invoke(lib, NewString("main"), 0, nullptr));
// Examine the result.
const int64_t prologue_count = get_integer(lib, "prologueCount");
const int64_t epilogue_count = get_integer(lib, "epilogueCount");
// We always call the "foo" function when its optimized (and it's optimized
// code has the call to `idempotentForceOptimizedFunction` inlined).
//
// The `idempotentForceOptimizedFunction` will always execute prologue,
// lazy-deoptimize the `foo` frame, that will make the `foo` re-try the call
// to `idempotentForceOptimizedFunction`.
// = > We should see the prologue of the force-optimized function to be
// executed twice as many times as epilogue.
EXPECT_EQ(epilogue_count * 2, prologue_count);
}
} // namespace dart