// Copyright (c) 2020, 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/frontend/kernel_binary_flowgraph.h"

#include "vm/compiler/backend/il_test_helper.h"
#include "vm/object.h"
#include "vm/unit_test.h"

namespace dart {

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_ConstFoldStringConcats) {
  // According to the Dart spec:
  // "Adjacent strings are implicitly concatenated to form a single string
  // literal."
  const char* kScript = R"(
    @pragma("vm:entry-point", "call")
    test() {
      var s = 'aaaa'
          'bbbb'
          'cccc';
      return s;
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "test");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  DartReturnInstr* ret = nullptr;

  ILMatcher cursor(flow_graph, entry);
  // clang-format off
  RELEASE_ASSERT(cursor.TryMatch({
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    {kMatchDartReturn, &ret},
  }));
  // clang-format on

  EXPECT(ret->value()->BindsToConstant());
  EXPECT(ret->value()->BoundConstant().IsString());
  const String& ret_str = String::Cast(ret->value()->BoundConstant());
  EXPECT(ret_str.Equals("aaaabbbbcccc"));
}

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_FlattenNestedStringInterp) {
  // We should collapse nested StringInterpolates:
  const char* kScript = R"(
    test(String s) {
      return '$s' '${'d' 'e'}';
    }
    main() => test('u');
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "main");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  StoreIndexedInstr* store1 = nullptr;
  StoreIndexedInstr* store2 = nullptr;

  ILMatcher cursor(flow_graph, entry);
  // clang-format off
  RELEASE_ASSERT(cursor.TryMatch({
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    kMatchAndMoveCreateArray,
    {kMatchAndMoveStoreIndexed, &store1},
    {kMatchAndMoveStoreIndexed, &store2},
    kMatchAndMoveRecordCoverage,
    kMatchAndMoveStaticCall,
    kMoveDebugStepChecks,
    kMatchDartReturn,
  }));
  // clang-format on

  // StoreIndexed(tmp_array, 0, s)
  EXPECT(store1->index()->BindsToConstant());
  EXPECT(store1->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store1->index()->BoundConstant()).Value() == 0);

  EXPECT(!store1->value()->BindsToConstant());

  // StoreIndexed(tmp_array, 1, "de")
  EXPECT(store2->index()->BindsToConstant());
  EXPECT(store2->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store2->index()->BoundConstant()).Value() == 1);

  EXPECT(store2->value()->BindsToConstant());
  EXPECT(store2->value()->BoundConstant().IsString());
  EXPECT(String::Cast(store2->value()->BoundConstant()).Equals("de"));
}

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_DropEmptyStringInterp) {
  // We should drop empty strings from StringInterpolates:
  const char* kScript = R"(
    test(s) {
      return '' 'a' '$s' '' 'b' '';
    }
    main() => test('u');
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "main");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  StoreIndexedInstr* store1 = nullptr;
  StoreIndexedInstr* store2 = nullptr;
  StoreIndexedInstr* store3 = nullptr;

  ILMatcher cursor(flow_graph, entry);
  // clang-format off
  RELEASE_ASSERT(cursor.TryMatch({
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    kMatchAndMoveCreateArray,
    {kMatchAndMoveStoreIndexed, &store1},
    {kMatchAndMoveStoreIndexed, &store2},
    {kMatchAndMoveStoreIndexed, &store3},
    kMatchAndMoveRecordCoverage,
    kMatchAndMoveStaticCall,
    kMoveDebugStepChecks,
    kMatchDartReturn,
  }));
  // clang-format on

  // StoreIndexed(tmp_array, 0, "ab")
  EXPECT(store1->index()->BindsToConstant());
  EXPECT(store1->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store1->index()->BoundConstant()).Value() == 0);

  EXPECT(store1->value()->BindsToConstant());
  EXPECT(store1->value()->BoundConstant().IsString());
  EXPECT(String::Cast(store1->value()->BoundConstant()).Equals("a"));

  // StoreIndexed(tmp_array, 1, s)
  EXPECT(store2->index()->BindsToConstant());
  EXPECT(store2->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store2->index()->BoundConstant()).Value() == 1);

  EXPECT(!store2->value()->BindsToConstant());

  // StoreIndexed(tmp_array, 2, "b")
  EXPECT(store3->index()->BindsToConstant());
  EXPECT(store3->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store3->index()->BoundConstant()).Value() == 2);

  EXPECT(store3->value()->BindsToConstant());
  EXPECT(store3->value()->BoundConstant().IsString());
  EXPECT(String::Cast(store3->value()->BoundConstant()).Equals("b"));
}

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_ConcatStringLits) {
  // We should drop empty strings from StringInterpolates:
  const char* kScript = R"(
    test(s) {
      return '' 'a' '' 'b' '$s' '' 'c' '' 'd' '';
    }
    main() => test('u');
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "main");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  StoreIndexedInstr* store1 = nullptr;
  StoreIndexedInstr* store2 = nullptr;
  StoreIndexedInstr* store3 = nullptr;

  ILMatcher cursor(flow_graph, entry);
  // clang-format off
  RELEASE_ASSERT(cursor.TryMatch({
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    kMatchAndMoveCreateArray,
    {kMatchAndMoveStoreIndexed, &store1},
    {kMatchAndMoveStoreIndexed, &store2},
    {kMatchAndMoveStoreIndexed, &store3},
    kMatchAndMoveRecordCoverage,
    kMatchAndMoveStaticCall,
    kMoveDebugStepChecks,
    kMatchDartReturn,
  }));
  // clang-format on

  // StoreIndexed(tmp_array, 0, "ab")
  EXPECT(store1->index()->BindsToConstant());
  EXPECT(store1->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store1->index()->BoundConstant()).Value() == 0);

  EXPECT(store1->value()->BindsToConstant());
  EXPECT(store1->value()->BoundConstant().IsString());
  EXPECT(String::Cast(store1->value()->BoundConstant()).Equals("ab"));

  // StoreIndexed(tmp_array, 1, s)
  EXPECT(store2->index()->BindsToConstant());
  EXPECT(store2->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store2->index()->BoundConstant()).Value() == 1);

  EXPECT(!store2->value()->BindsToConstant());

  // StoreIndexed(tmp_array, 2, "cd")
  EXPECT(store3->index()->BindsToConstant());
  EXPECT(store3->index()->BoundConstant().IsInteger());
  EXPECT(Integer::Cast(store3->index()->BoundConstant()).Value() == 2);

  EXPECT(store3->value()->BindsToConstant());
  EXPECT(store3->value()->BoundConstant().IsString());
  EXPECT(String::Cast(store3->value()->BoundConstant()).Equals("cd"));
}

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_InvariantFlagInListLiterals) {
  const char* kScript = R"(
    @pragma("vm:entry-point", "call")
    test() {
      return [...[], 42];
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "test");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  InstanceCallInstr* call_add = nullptr;

  ILMatcher cursor(flow_graph, entry);
  // clang-format off
  RELEASE_ASSERT(cursor.TryMatch({
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    kMatchAndMoveRecordCoverage,
    kMatchAndMoveStaticCall,
    kMatchAndMoveRecordCoverage,
    kMatchAndMoveStaticCall,
    kMatchAndMoveRecordCoverage,
    {kMatchAndMoveInstanceCall, &call_add},
    kMoveDebugStepChecks,
    kMatchDartReturn,
  }));
  // clang-format on

  EXPECT(call_add != nullptr);
  EXPECT(call_add->function_name().Equals("add"));
  EXPECT(call_add->entry_kind() == Code::EntryKind::kUnchecked);
}

ISOLATE_UNIT_TEST_CASE(StreamingFlowGraphBuilder_TypedClosureCall) {
  // This test ensures that a typed closure call (i.e. the closure being called
  // has a real function type as static type) ends up compiling to
  //
  //   CheckNull()+ClosureCall()
  //
  // instead of
  //
  //   InstanceCall(dyn:call)
  //
  // This is a regression test for a case where JIT support uses dynamic calls
  // instead of typed closure calls if call-site attribute metadata is missing
  // which is the case if an incremental kernel compiler is used. The
  // [LoadTestScript] below uses IKG compiler, so this is a regression test for:
  //
  //   - https://github.com/dart-lang/sdk/issues/46320
  //   - https://github.com/dart-lang/sdk/issues/45421
  //
  const char* kScript = R"(
    int callClosure(int Function(int) fun, int value) => fun(value);
    @pragma("vm:entry-point", "call")
    test() => callClosure((int a) => a + 1, 10);
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  Invoke(root_library, "test");

  const auto& callClosureFunction =
      Function::Handle(GetFunction(root_library, "callClosure"));
  TestPipeline pipeline(callClosureFunction, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  ILMatcher cursor(flow_graph, entry, true);
  // clang-format off
  std::initializer_list<MatchCode> expected = {
    kMatchAndMoveFunctionEntry,
    kMatchAndMoveCheckStackOverflow,
    kMoveDebugStepChecks,
    kMatchAndMoveCheckNull,
    kMatchAndMoveLoadField,
    kMoveDebugStepChecks,
#if !defined(PRODUCT)
    kMatchAndMoveRecordCoverage,
#endif
    kMatchAndMoveClosureCall,
    kMoveDebugStepChecks,
    kMatchDartReturn,
  };
  RELEASE_ASSERT(cursor.TryMatch(expected));
  // clang-format on
}

ISOLATE_UNIT_TEST_CASE(
    StreamingFlowGraphBuilder_StaticGetFinalFieldWithTrivialInitializer) {
  const char* kScript = R"(
    final int x = 0xFEEDFEED;
    @pragma("vm:entry-point", "call")
    test() {
      return x;
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  const auto& function = Function::Handle(GetFunction(root_library, "test"));

  Invoke(root_library, "test");

  TestPipeline pipeline(function, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  DartReturnInstr* return_instr = nullptr;

  ILMatcher cursor(flow_graph, entry);
  RELEASE_ASSERT(cursor.TryMatch({
      kMatchAndMoveFunctionEntry,
      kMatchAndMoveCheckStackOverflow,
      kMoveDebugStepChecks,
      {kMatchDartReturn, &return_instr},
  }));

  EXPECT(return_instr != nullptr);
  ConstantInstr* const_value =
      return_instr->value()->definition()->AsConstant();
  EXPECT(const_value != nullptr);
  EXPECT(const_value->value().IsInteger());
  EXPECT_EQ(0xFEEDFEED, Integer::Cast(const_value->value()).Value());
}

ISOLATE_UNIT_TEST_CASE(
    StreamingFlowGraphBuilder_DeeplyImmutableTypeCheckPresent) {
  const char* kScript = R"(
    @pragma("vm:deeply-immutable")
    final class Foo {
      final void Function(int) baz;
      Foo(this.baz);
    }

    @pragma("vm:entry-point", "call")
    test() {
      Foo((int x) { x = x + 1; } );
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  EXPECT(ClassFinalizer::ProcessPendingClasses());
  const Class& foo = Class::Handle(GetClass(root_library, "Foo"));
  const auto& error = foo.EnsureIsFinalized(thread);
  const auto& constructor = Function::Handle(
      foo.LookupConstructor(String::Handle(String::New("Foo."))));

  EXPECT(error == Error::null());
  Invoke(root_library, "test");

  TestPipeline pipeline(constructor, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  ILMatcher cursor(flow_graph, entry);
  RELEASE_ASSERT(cursor.TryMatch({
      kMatchAndMoveFunctionEntry,
      kMatchAndMoveCheckStackOverflow,
      kMoveDebugStepChecks,
      kMatchAndMoveGuardFieldClass,
      kMatchAndMoveCheckFieldImmutability,
      kMatchAndMoveStoreField,
  }));
}

ISOLATE_UNIT_TEST_CASE(
    StreamingFlowGraphBuilder_DeeplyImmutableTypeCheckOmitted) {
  const char* kScript = R"(
    @pragma("vm:deeply-immutable")
    final class Foo {
      final int baz;
      Foo(this.baz);
    }

    @pragma("vm:entry-point", "call")
    test() {
      Foo(42);
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  EXPECT(ClassFinalizer::ProcessPendingClasses());
  const Class& foo = Class::Handle(GetClass(root_library, "Foo"));
  const auto& error = foo.EnsureIsFinalized(thread);
  const auto& constructor = Function::Handle(
      foo.LookupConstructor(String::Handle(String::New("Foo."))));

  EXPECT(error == Error::null());

  Invoke(root_library, "test");

  TestPipeline pipeline(constructor, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  ILMatcher cursor(flow_graph, entry);
  RELEASE_ASSERT(cursor.TryMatch({
      kMatchAndMoveFunctionEntry,
      kMatchAndMoveCheckStackOverflow,
      kMoveDebugStepChecks,
      kMatchAndMoveGuardFieldClass,
      kMatchAndMoveStoreField,
  }));
}

ISOLATE_UNIT_TEST_CASE(
    StreamingFlowGraphBuilder_MapDeeplyImmutableTypeCheckPresentAndFailing) {
  const char* kScript = R"(
    @pragma("vm:deeply-immutable")
    final class Foo {
      final Map map;
      Foo(this.map);
    }

    @pragma("vm:entry-point", "call")
    test() {
      final map = <int, int>{1: 2, 3: 4};
      bool caught = false;
      try {
        Foo(map);
      } on ArgumentError catch(_) {
        caught = true;
      }
      // plain Map should not pass deep-immutability check at this point.
      if (!caught) {
        throw Exception();
      }
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  EXPECT(ClassFinalizer::ProcessPendingClasses());
  const Class& foo = Class::Handle(GetClass(root_library, "Foo"));
  const auto& error = foo.EnsureIsFinalized(thread);
  const auto& constructor = Function::Handle(
      foo.LookupConstructor(String::Handle(String::New("Foo."))));

  EXPECT(error == Error::null());
  Invoke(root_library, "test");

  TestPipeline pipeline(constructor, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  ILMatcher cursor(flow_graph, entry);
  RELEASE_ASSERT(cursor.TryMatch({
      kMatchAndMoveFunctionEntry,
      kMatchAndMoveCheckStackOverflow,
      kMoveDebugStepChecks,
      kMatchAndMoveGuardFieldClass,
      kMoveGlob,
      kMatchAndMoveCheckFieldImmutability,
      kMatchAndMoveStoreField,
  }));
}

ISOLATE_UNIT_TEST_CASE(
    StreamingFlowGraphBuilder_ConstMapDeeplyImmutableTypeCheckPresent) {
  const char* kScript = R"(
    @pragma("vm:deeply-immutable")
    final class Foo {
      final Map map;
      Foo(this.map);
    }

    @pragma("vm:entry-point", "call")
    test() {
      const map = const <int, int>{1: 2, 3: 4};
      // const Map should pass deep-immutability check.
      Foo(map);
    }
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  EXPECT(ClassFinalizer::ProcessPendingClasses());
  const Class& foo = Class::Handle(GetClass(root_library, "Foo"));
  const auto& error = foo.EnsureIsFinalized(thread);
  const auto& constructor = Function::Handle(
      foo.LookupConstructor(String::Handle(String::New("Foo."))));

  EXPECT(error == Error::null());
  Invoke(root_library, "test");

  TestPipeline pipeline(constructor, CompilerPass::kJIT);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
  });

  auto entry = flow_graph->graph_entry()->normal_entry();
  EXPECT(entry != nullptr);

  ILMatcher cursor(flow_graph, entry);
  RELEASE_ASSERT(cursor.TryMatch({
      kMatchAndMoveFunctionEntry,
      kMatchAndMoveCheckStackOverflow,
      kMoveDebugStepChecks,
      kMatchAndMoveGuardFieldClass,
      kMoveGlob,
      kMatchAndMoveCheckFieldImmutability,
      kMatchAndMoveStoreField,
  }));
}

}  // namespace dart
