// 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 EqualityCompareInstr* check =
        current->AsBranch()->condition()->AsEqualityCompare();
    EXPECT(check != nullptr);
    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);
}

#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,
      kMatchDartReturn,
  }));
}

// 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,
      kMatchDartReturn,
  }));
  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,
      kMatchDartReturn,
  }));
  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"(
    @pragma('vm:force-optimize')
    @pragma('vm:idempotent')
    @pragma('vm:prefer-inline')
    int newHash() => identityHashCode(Object());

    @pragma('vm:never-inline')
    int foo() {
      int x = newHash();
      return x + 1;
    }
    main() {
      int r = 0;
      for (int i = 0; i < 1000; i++) {
        r += foo();
      }
      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(), "newHash") ==
           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());
  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::kFixedOOMStackdepth);

  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() {}

@pragma('vm:entry-point', 'get')
int prologueCount = 0;
@pragma('vm:entry-point', 'get')
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
