// Copyright (c) 2012, 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/il.h"

#include <optional>
#include <vector>

#include "platform/text_buffer.h"
#include "platform/thread_sanitizer.h"
#include "platform/utils.h"
#include "vm/class_id.h"
#include "vm/compiler/assembler/disassembler.h"
#include "vm/compiler/backend/block_builder.h"
#include "vm/compiler/backend/flow_graph_compiler.h"
#include "vm/compiler/backend/il_printer.h"
#include "vm/compiler/backend/il_test_helper.h"
#include "vm/compiler/backend/range_analysis.h"
#include "vm/compiler/backend/type_propagator.h"
#include "vm/unit_test.h"

namespace dart {

ISOLATE_UNIT_TEST_CASE(InstructionTests) {
  TargetEntryInstr* target_instr =
      new TargetEntryInstr(1, kInvalidTryIndex, DeoptId::kNone);
  EXPECT(target_instr->IsBlockEntry());
  EXPECT(!target_instr->IsDefinition());
}

ISOLATE_UNIT_TEST_CASE(OptimizationTests) {
  JoinEntryInstr* join =
      new JoinEntryInstr(1, kInvalidTryIndex, DeoptId::kNone);

  Definition* def1 = new PhiInstr(join, 0);
  Definition* def2 = new PhiInstr(join, 0);
  Value* use1a = new Value(def1);
  Value* use1b = new Value(def1);
  EXPECT(use1a->Equals(*use1b));
  Value* use2 = new Value(def2);
  EXPECT(!use2->Equals(*use1a));

  ConstantInstr* c1 = new ConstantInstr(Bool::True());
  ConstantInstr* c2 = new ConstantInstr(Bool::True());
  EXPECT(c1->Equals(*c2));
  ConstantInstr* c3 = new ConstantInstr(Object::ZoneHandle());
  ConstantInstr* c4 = new ConstantInstr(Object::ZoneHandle());
  EXPECT(c3->Equals(*c4));
  EXPECT(!c3->Equals(*c1));
}

ISOLATE_UNIT_TEST_CASE(IRTest_EliminateWriteBarrier) {
  // clang-format off
  const char* kScript = R"(
      class Container<T> {
        operator []=(index, value) {
          return data[index] = value;
        }

        List<T?> data = List<T?>.filled(10, null);
      }

      Container<int> x = Container<int>();

      @pragma("vm:entry-point", "call")
      foo() {
        for (int i = 0; i < 10; ++i) {
          x[i] = i;
        }
      }
    )";
  // clang-format on

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

  Invoke(root_library, "foo");

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

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

  StoreIndexedInstr* store_indexed = nullptr;

  ILMatcher cursor(flow_graph, entry, true);
  RELEASE_ASSERT(cursor.TryMatch({
      kMoveGlob,
      kMatchAndMoveBranchTrue,
      kMoveGlob,
      {kMatchStoreIndexed, &store_indexed},
  }));

  EXPECT(!store_indexed->value()->NeedsWriteBarrier());
}

static void ExpectStores(FlowGraph* flow_graph,
                         const std::vector<const char*>& expected_stores) {
  size_t next_expected_store = 0;
  for (BlockIterator block_it = flow_graph->reverse_postorder_iterator();
       !block_it.Done(); block_it.Advance()) {
    for (ForwardInstructionIterator it(block_it.Current()); !it.Done();
         it.Advance()) {
      if (auto store = it.Current()->AsStoreField()) {
        EXPECT_LT(next_expected_store, expected_stores.size());
        EXPECT_STREQ(expected_stores[next_expected_store],
                     store->slot().Name());
        next_expected_store++;
      }
    }
  }
}

static void RunInitializingStoresTest(
    const Library& root_library,
    const char* function_name,
    CompilerPass::PipelineMode mode,
    const std::vector<const char*>& expected_stores) {
  const auto& function =
      Function::Handle(GetFunction(root_library, function_name));
  TestPipeline pipeline(function, mode);
  FlowGraph* flow_graph = pipeline.RunPasses({
      CompilerPass::kComputeSSA,
      CompilerPass::kTypePropagation,
      CompilerPass::kApplyICData,
      CompilerPass::kInlining,
      CompilerPass::kTypePropagation,
      CompilerPass::kSelectRepresentations,
      CompilerPass::kCanonicalize,
      CompilerPass::kConstantPropagation,
  });
  ASSERT(flow_graph != nullptr);
  ExpectStores(flow_graph, expected_stores);
}

ISOLATE_UNIT_TEST_CASE(IRTest_InitializingStores) {
  // clang-format off
  const char* kScript = R"(
    class Bar {
      var f;
      var g;

      Bar({this.f, this.g});
    }
    Bar f1() => Bar(f: 10);
    Bar f2() => Bar(g: 10);
    f3() {
      return () { };
    }
    f4<T>({T? value}) {
      return () { return value; };
    }
    main() {
      f1();
      f2();
      f3();
      f4();
    }
  )";
  // clang-format on

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

  RunInitializingStoresTest(root_library, "f1", CompilerPass::kJIT,
                            /*expected_stores=*/{"f"});
  RunInitializingStoresTest(root_library, "f2", CompilerPass::kJIT,
                            /*expected_stores=*/{"g"});
  RunInitializingStoresTest(root_library, "f3", CompilerPass::kJIT,
                            /*expected_stores=*/
                            {"Closure.function", "Closure.entry_point"});

  // Note that in JIT mode we lower context allocation in a way that hinders
  // removal of initializing moves so there would be some redundant stores of
  // null left in the graph. In AOT mode we don't apply this optimization
  // which enables us to remove more stores.
  std::vector<const char*> expected_stores_jit;
  std::vector<const char*> expected_stores_aot;

  expected_stores_jit.insert(
      expected_stores_jit.end(),
      {"value", "Context.parent", "Context.parent", "value",
       ":closure_element[0]", ":closure_element[0]"});
  expected_stores_aot.insert(
      expected_stores_aot.end(),
      {"value", ":closure_element[0]", ":closure_element[0]"});

  RunInitializingStoresTest(root_library, "f4", CompilerPass::kJIT,
                            expected_stores_jit);
  RunInitializingStoresTest(root_library, "f4", CompilerPass::kAOT,
                            expected_stores_aot);
}

// Returns |true| if compiler canonicalizes away a chain of IntConverters going
// from |initial| representation to |intermediate| representation and then
// back to |initial| given that initial value has range [min_value, max_value].
bool TestIntConverterCanonicalizationRule(Thread* thread,
                                          int64_t min_value,
                                          int64_t max_value,
                                          Representation initial,
                                          Representation intermediate,
                                          Representation final) {
  using compiler::BlockBuilder;

  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);

  FlowGraphBuilderHelper H(/*num_parameters=*/1);
  H.AddVariable("v0", AbstractType::ZoneHandle(Type::IntType()));

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();

  Definition* v0;
  DartReturnInstr* ret;

  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    v0 = builder.AddParameter(0, initial);
    v0->set_range(Range(RangeBoundary::FromConstant(min_value),
                        RangeBoundary::FromConstant(max_value)));
    auto conv1 = builder.AddDefinition(
        new IntConverterInstr(initial, intermediate, new Value(v0)));
    auto conv2 = builder.AddDefinition(
        new IntConverterInstr(intermediate, initial, new Value(conv1)));
    ret = builder.AddReturn(new Value(conv2));
  }

  H.FinishGraph();

  H.flow_graph()->Canonicalize();
  H.flow_graph()->Canonicalize();

  return ret->value()->definition() == v0;
}

ISOLATE_UNIT_TEST_CASE(IL_IntConverterCanonicalization) {
  EXPECT(TestIntConverterCanonicalizationRule(thread, kMinInt16, kMaxInt16,
                                              kUnboxedInt64, kUnboxedInt32,
                                              kUnboxedInt64));
  EXPECT(TestIntConverterCanonicalizationRule(thread, kMinInt32, kMaxInt32,
                                              kUnboxedInt64, kUnboxedInt32,
                                              kUnboxedInt64));
  EXPECT(!TestIntConverterCanonicalizationRule(
      thread, kMinInt32, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedInt64,
      kUnboxedInt32, kUnboxedInt64));
  EXPECT(TestIntConverterCanonicalizationRule(
      thread, 0, kMaxInt16, kUnboxedInt64, kUnboxedUint32, kUnboxedInt64));
  EXPECT(TestIntConverterCanonicalizationRule(
      thread, 0, kMaxInt32, kUnboxedInt64, kUnboxedUint32, kUnboxedInt64));
  EXPECT(TestIntConverterCanonicalizationRule(
      thread, 0, kMaxUint32, kUnboxedInt64, kUnboxedUint32, kUnboxedInt64));
  EXPECT(!TestIntConverterCanonicalizationRule(
      thread, 0, static_cast<int64_t>(kMaxUint32) + 1, kUnboxedInt64,
      kUnboxedUint32, kUnboxedInt64));
  EXPECT(!TestIntConverterCanonicalizationRule(
      thread, -1, kMaxInt16, kUnboxedInt64, kUnboxedUint32, kUnboxedInt64));

  // Regression test for https://dartbug.com/53613.
  EXPECT(!TestIntConverterCanonicalizationRule(thread, kMinInt32, kMaxInt32,
                                               kUnboxedInt32, kUnboxedUint32,
                                               kUnboxedInt64));
  EXPECT(!TestIntConverterCanonicalizationRule(thread, kMinInt32, kMaxInt32,
                                               kUnboxedInt32, kUnboxedUint32,
                                               kUnboxedInt32));
  EXPECT(TestIntConverterCanonicalizationRule(
      thread, 0, kMaxInt32, kUnboxedInt32, kUnboxedUint32, kUnboxedInt64));
  EXPECT(TestIntConverterCanonicalizationRule(
      thread, 0, kMaxInt32, kUnboxedInt32, kUnboxedUint32, kUnboxedInt32));
}

ISOLATE_UNIT_TEST_CASE(IL_PhiCanonicalization) {
  using compiler::BlockBuilder;

  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);

  FlowGraphBuilderHelper H(/*num_parameters=*/1);
  H.AddVariable("v0", AbstractType::ZoneHandle(Type::DynamicType()));

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();
  auto b2 = H.JoinEntry();
  auto b3 = H.TargetEntry();
  auto b4 = H.TargetEntry();

  Definition* v0;
  DartReturnInstr* ret;
  PhiInstr* phi;

  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    v0 = builder.AddParameter(0, kTagged);
    builder.AddInstruction(new GotoInstr(b2, S.GetNextDeoptId()));
  }

  {
    BlockBuilder builder(H.flow_graph(), b2);
    phi = new PhiInstr(b2, 2);
    phi->SetInputAt(0, new Value(v0));
    phi->SetInputAt(1, new Value(phi));
    builder.AddPhi(phi);
    builder.AddBranch(new StrictCompareInstr(
                          InstructionSource(), Token::kEQ_STRICT,
                          new Value(H.IntConstant(1)), new Value(phi),
                          /*needs_number_check=*/false, S.GetNextDeoptId()),
                      b3, b4);
  }

  {
    BlockBuilder builder(H.flow_graph(), b3);
    builder.AddInstruction(new GotoInstr(b2, S.GetNextDeoptId()));
  }

  {
    BlockBuilder builder(H.flow_graph(), b4);
    ret = builder.AddReturn(new Value(phi));
  }

  H.FinishGraph();

  H.flow_graph()->Canonicalize();

  EXPECT(ret->value()->definition() == v0);
}

// Regression test for issue 46018.
ISOLATE_UNIT_TEST_CASE(IL_UnboxIntegerCanonicalization) {
  using compiler::BlockBuilder;

  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);

  FlowGraphBuilderHelper H(/*num_parameters=*/2);
  H.AddVariable("v0", AbstractType::ZoneHandle(Type::DynamicType()));
  H.AddVariable("v1", AbstractType::ZoneHandle(Type::DynamicType()));

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();
  Definition* unbox;

  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    Definition* index = H.IntConstant(0);
    Definition* int_type =
        H.flow_graph()->GetConstant(Type::Handle(Type::IntType()));

    Definition* float64_array = builder.AddParameter(0, kTagged);
    Definition* int64_array = builder.AddParameter(1, kTagged);

    Definition* load_indexed = builder.AddDefinition(new LoadIndexedInstr(
        new Value(float64_array), new Value(index),
        /* index_unboxed */ false,
        /* index_scale */ 8, kTypedDataFloat64ArrayCid, kAlignedAccess,
        S.GetNextDeoptId(), InstructionSource()));
    Definition* box = builder.AddDefinition(
        BoxInstr::Create(kUnboxedDouble, new Value(load_indexed)));
    Definition* cast = builder.AddDefinition(new AssertAssignableInstr(
        InstructionSource(), new Value(box), new Value(int_type),
        /* instantiator_type_arguments */
        new Value(H.flow_graph()->constant_null()),
        /* function_type_arguments */
        new Value(H.flow_graph()->constant_null()),
        /* dst_name */ String::Handle(String::New("not-null")),
        S.GetNextDeoptId()));
    unbox = builder.AddDefinition(
        new UnboxInt64Instr(new Value(cast), S.GetNextDeoptId(),
                            UnboxInstr::ValueMode::kCheckType));

    builder.AddInstruction(new StoreIndexedInstr(
        new Value(int64_array), new Value(index), new Value(unbox),
        kNoStoreBarrier,
        /* index_unboxed */ false,
        /* index_scale */ 8, kTypedDataInt64ArrayCid, kAlignedAccess,
        S.GetNextDeoptId(), InstructionSource()));
    builder.AddReturn(new Value(index));
  }

  H.FinishGraph();

  FlowGraphTypePropagator::Propagate(H.flow_graph());
  EXPECT(unbox->ComputeCanDeoptimize());

  H.flow_graph()->Canonicalize();
  EXPECT(!unbox->ComputeCanDeoptimize());

  H.flow_graph()->RemoveRedefinitions();
  EXPECT(!unbox->ComputeCanDeoptimize());  // Previously this reverted to true.
}

static void WriteCidTo(intptr_t cid, BaseTextBuffer* buffer) {
  ClassTable* const class_table = IsolateGroup::Current()->class_table();
  buffer->Printf("%" Pd "", cid);
  if (class_table->HasValidClassAt(cid)) {
    const auto& cls = Class::Handle(class_table->At(cid));
    buffer->Printf(" (%s", cls.ScrubbedNameCString());
    if (cls.is_abstract()) {
      buffer->AddString(", abstract");
    }
    buffer->AddString(")");
  }
}

static void TestNullAwareEqualityCompareCanonicalization(
    Thread* thread,
    bool allow_representation_change) {
  using compiler::BlockBuilder;

  CompilerState S(thread, /*is_aot=*/true, /*is_optimizing=*/true);

  FlowGraphBuilderHelper H(/*num_parameters=*/2);
  H.AddVariable("v0", AbstractType::ZoneHandle(Type::IntType()));
  H.AddVariable("v1", AbstractType::ZoneHandle(Type::IntType()));

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();

  EqualityCompareInstr* compare = nullptr;
  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    Definition* v0 = builder.AddParameter(0, kUnboxedInt64);
    Definition* v1 = builder.AddParameter(1, kUnboxedInt64);
    Definition* box0 = builder.AddDefinition(new BoxInt64Instr(new Value(v0)));
    Definition* box1 = builder.AddDefinition(new BoxInt64Instr(new Value(v1)));

    compare = builder.AddDefinition(new EqualityCompareInstr(
        InstructionSource(), Token::kEQ, new Value(box0), new Value(box1),
        kTagged, S.GetNextDeoptId(), /*null_aware=*/true));
    builder.AddReturn(new Value(compare));
  }

  H.FinishGraph();

  if (!allow_representation_change) {
    H.flow_graph()->disallow_unmatched_representations();
  }

  H.flow_graph()->Canonicalize();

  EXPECT(compare->is_null_aware() == !allow_representation_change);
  EXPECT(compare->input_representation() ==
         (allow_representation_change ? kUnboxedInt64 : kTagged));
}

ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_EqualityCompare) {
  TestNullAwareEqualityCompareCanonicalization(thread, true);
  TestNullAwareEqualityCompareCanonicalization(thread, false);
}

static void WriteCidRangeVectorTo(const CidRangeVector& ranges,
                                  BaseTextBuffer* buffer) {
  if (ranges.is_empty()) {
    buffer->AddString("empty CidRangeVector");
    return;
  }
  buffer->AddString("non-empty CidRangeVector:\n");
  for (const auto& range : ranges) {
    for (intptr_t cid = range.cid_start; cid <= range.cid_end; cid++) {
      buffer->AddString("  * ");
      WriteCidTo(cid, buffer);
      buffer->AddString("\n");
    }
  }
}

static bool ExpectRangesContainCid(const Expect& expect,
                                   const CidRangeVector& ranges,
                                   intptr_t expected) {
  for (const auto& range : ranges) {
    for (intptr_t cid = range.cid_start; cid <= range.cid_end; cid++) {
      if (expected == cid) return true;
    }
  }
  TextBuffer buffer(128);
  buffer.AddString("Expected CidRangeVector to include cid ");
  WriteCidTo(expected, &buffer);
  expect.Fail("%s", buffer.buffer());
  return false;
}

static void RangesContainExpectedCids(const Expect& expect,
                                      const CidRangeVector& ranges,
                                      const GrowableArray<intptr_t>& expected) {
  ASSERT(!ranges.is_empty());
  ASSERT(!expected.is_empty());
  {
    TextBuffer buffer(128);
    buffer.AddString("Checking that ");
    WriteCidRangeVectorTo(ranges, &buffer);
    buffer.AddString("includes cids:\n");
    for (const intptr_t cid : expected) {
      buffer.AddString("  * ");
      WriteCidTo(cid, &buffer);
      buffer.AddString("\n");
    }
    THR_Print("%s", buffer.buffer());
  }
  bool all_found = true;
  for (const intptr_t cid : expected) {
    if (!ExpectRangesContainCid(expect, ranges, cid)) {
      all_found = false;
    }
  }
  if (all_found) {
    THR_Print("All expected cids included.\n\n");
  }
}

#define RANGES_CONTAIN_EXPECTED_CIDS(ranges, cids)                             \
  RangesContainExpectedCids(dart::Expect(__FILE__, __LINE__), ranges, cids)

ISOLATE_UNIT_TEST_CASE(HierarchyInfo_Object_Subtype) {
  HierarchyInfo hi(thread);
  const auto& type =
      Type::Handle(IsolateGroup::Current()->object_store()->object_type());
  const bool is_nullable = Instance::NullIsAssignableTo(type);
  EXPECT(hi.CanUseSubtypeRangeCheckFor(type));
  const auto& cls = Class::Handle(type.type_class());

  ClassTable* const class_table = thread->isolate_group()->class_table();
  const intptr_t num_cids = class_table->NumCids();
  auto& to_check = Class::Handle(thread->zone());
  auto& rare_type = AbstractType::Handle(thread->zone());

  GrowableArray<intptr_t> expected_concrete_cids;
  GrowableArray<intptr_t> expected_abstract_cids;
  for (intptr_t cid = kInstanceCid; cid < num_cids; cid++) {
    if (!class_table->HasValidClassAt(cid)) continue;
    if (cid == kNullCid) continue;
    if (cid == kNeverCid) continue;
    if (cid == kDynamicCid && !is_nullable) continue;
    if (cid == kVoidCid && !is_nullable) continue;
    to_check = class_table->At(cid);
    // Only add concrete classes.
    if (to_check.is_abstract()) {
      expected_abstract_cids.Add(cid);
    } else {
      expected_concrete_cids.Add(cid);
    }
    if (cid != kTypeArgumentsCid) {  // Cannot call RareType() on this.
      rare_type = to_check.RareType();
      EXPECT(rare_type.IsSubtypeOf(type, Heap::kNew));
    }
  }

  const CidRangeVector& concrete_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/false, /*exclude_null=*/!is_nullable);
  RANGES_CONTAIN_EXPECTED_CIDS(concrete_range, expected_concrete_cids);

  GrowableArray<intptr_t> expected_cids;
  expected_cids.AddArray(expected_concrete_cids);
  expected_cids.AddArray(expected_abstract_cids);
  const CidRangeVector& abstract_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/true, /*exclude_null=*/!is_nullable);
  RANGES_CONTAIN_EXPECTED_CIDS(abstract_range, expected_cids);
}

ISOLATE_UNIT_TEST_CASE(HierarchyInfo_Function_Subtype) {
  HierarchyInfo hi(thread);
  const auto& type =
      Type::Handle(IsolateGroup::Current()->object_store()->function_type());
  EXPECT(hi.CanUseSubtypeRangeCheckFor(type));
  const auto& cls = Class::Handle(type.type_class());

  GrowableArray<intptr_t> expected_concrete_cids;
  expected_concrete_cids.Add(kClosureCid);

  GrowableArray<intptr_t> expected_abstract_cids;
  expected_abstract_cids.Add(type.type_class_id());

  const CidRangeVector& concrete_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/false, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(concrete_range, expected_concrete_cids);

  GrowableArray<intptr_t> expected_cids;
  expected_cids.AddArray(expected_concrete_cids);
  expected_cids.AddArray(expected_abstract_cids);
  const CidRangeVector& abstract_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/true, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(abstract_range, expected_cids);
}

ISOLATE_UNIT_TEST_CASE(HierarchyInfo_Num_Subtype) {
  HierarchyInfo hi(thread);
  const auto& num_type = Type::Handle(Type::Number());
  const auto& int_type = Type::Handle(Type::IntType());
  const auto& double_type = Type::Handle(Type::Double());
  EXPECT(hi.CanUseSubtypeRangeCheckFor(num_type));
  const auto& cls = Class::Handle(num_type.type_class());

  GrowableArray<intptr_t> expected_concrete_cids;
  expected_concrete_cids.Add(kSmiCid);
  expected_concrete_cids.Add(kMintCid);
  expected_concrete_cids.Add(kDoubleCid);

  GrowableArray<intptr_t> expected_abstract_cids;
  expected_abstract_cids.Add(num_type.type_class_id());
  expected_abstract_cids.Add(int_type.type_class_id());
  expected_abstract_cids.Add(double_type.type_class_id());

  const CidRangeVector& concrete_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/false, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(concrete_range, expected_concrete_cids);

  GrowableArray<intptr_t> expected_cids;
  expected_cids.AddArray(expected_concrete_cids);
  expected_cids.AddArray(expected_abstract_cids);
  const CidRangeVector& abstract_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/true, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(abstract_range, expected_cids);
}

ISOLATE_UNIT_TEST_CASE(HierarchyInfo_Int_Subtype) {
  HierarchyInfo hi(thread);
  const auto& type = Type::Handle(Type::IntType());
  EXPECT(hi.CanUseSubtypeRangeCheckFor(type));
  const auto& cls = Class::Handle(type.type_class());

  GrowableArray<intptr_t> expected_concrete_cids;
  expected_concrete_cids.Add(kSmiCid);
  expected_concrete_cids.Add(kMintCid);

  GrowableArray<intptr_t> expected_abstract_cids;
  expected_abstract_cids.Add(type.type_class_id());

  const CidRangeVector& concrete_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/false, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(concrete_range, expected_concrete_cids);

  GrowableArray<intptr_t> expected_cids;
  expected_cids.AddArray(expected_concrete_cids);
  expected_cids.AddArray(expected_abstract_cids);
  const CidRangeVector& abstract_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/true, /*exclude_null=*/true);
  RANGES_CONTAIN_EXPECTED_CIDS(abstract_range, expected_cids);
}

ISOLATE_UNIT_TEST_CASE(HierarchyInfo_String_Subtype) {
  HierarchyInfo hi(thread);
  const auto& type = Type::Handle(Type::StringType());
  EXPECT(hi.CanUseSubtypeRangeCheckFor(type));
  const auto& cls = Class::Handle(type.type_class());

  GrowableArray<intptr_t> expected_concrete_cids;
  expected_concrete_cids.Add(kOneByteStringCid);
  expected_concrete_cids.Add(kTwoByteStringCid);

  GrowableArray<intptr_t> expected_abstract_cids;
  expected_abstract_cids.Add(type.type_class_id());

  const CidRangeVector& concrete_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/false, /*exclude_null=*/true);
  THR_Print("Checking concrete subtype ranges for String\n");
  RANGES_CONTAIN_EXPECTED_CIDS(concrete_range, expected_concrete_cids);

  GrowableArray<intptr_t> expected_cids;
  expected_cids.AddArray(expected_concrete_cids);
  expected_cids.AddArray(expected_abstract_cids);
  const CidRangeVector& abstract_range = hi.SubtypeRangesForClass(
      cls, /*include_abstract=*/true, /*exclude_null=*/true);
  THR_Print("Checking concrete and abstract subtype ranges for String\n");
  RANGES_CONTAIN_EXPECTED_CIDS(abstract_range, expected_cids);
}

// This test verifies that double == Smi is recognized and
// implemented using EqualityCompare.
// Regression test for https://github.com/dart-lang/sdk/issues/47031.
ISOLATE_UNIT_TEST_CASE(IRTest_DoubleEqualsSmi) {
  const char* kScript = R"(
    bool foo(double x) => (x + 0.5) == 0;
    main() {
      foo(-0.5);
    }
  )";

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

ISOLATE_UNIT_TEST_CASE(IRTest_LoadThread) {
  // clang-format off
  auto kScript = R"(
    import 'dart:ffi';

    @pragma("vm:entry-point", "call")
    int myFunction() {
      return 100;
    }

    void anotherFunction() {}
  )";
  // clang-format on

  const auto& root_library = Library::Handle(LoadTestScript(kScript));
  Zone* const zone = Thread::Current()->zone();
  auto& invoke_result = Instance::Handle(zone);
  invoke_result ^= Invoke(root_library, "myFunction");
  EXPECT_EQ(Smi::New(100), invoke_result.ptr());

  const auto& my_function =
      Function::Handle(GetFunction(root_library, "myFunction"));

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

  DartReturnInstr* return_instr = nullptr;
  {
    ILMatcher cursor(flow_graph, flow_graph->graph_entry()->normal_entry());

    EXPECT(cursor.TryMatch({
        kMoveGlob,
        {kMatchDartReturn, &return_instr},
    }));
  }

  auto* const load_thread_instr = new (zone) LoadThreadInstr();
  flow_graph->InsertBefore(return_instr, load_thread_instr, nullptr,
                           FlowGraph::kValue);
  auto load_thread_value = Value(load_thread_instr);

  auto* const convert_instr = new (zone)
      IntConverterInstr(kUntagged, kUnboxedAddress, &load_thread_value);
  flow_graph->InsertBefore(return_instr, convert_instr, nullptr,
                           FlowGraph::kValue);
  auto convert_value = Value(convert_instr);

  auto* const box_instr = BoxInstr::Create(kUnboxedAddress, &convert_value);
  flow_graph->InsertBefore(return_instr, box_instr, nullptr, FlowGraph::kValue);

  return_instr->InputAt(0)->definition()->ReplaceUsesWith(box_instr);

  {
    // Check we constructed the right graph.
    ILMatcher cursor(flow_graph, flow_graph->graph_entry()->normal_entry());
    EXPECT(cursor.TryMatch({
        kMoveGlob,
        kMatchAndMoveLoadThread,
        kMatchAndMoveIntConverter,
        kMatchAndMoveBox,
        kMatchDartReturn,
    }));
  }

  pipeline.RunForcedOptimizedAfterSSAPasses();

  {
#if !defined(PRODUCT) && !defined(USING_THREAD_SANITIZER)
    SetFlagScope<bool> sfs(&FLAG_disassemble_optimized, true);
#endif
    pipeline.CompileGraphAndAttachFunction();
  }

  // Ensure we can successfully invoke the function.
  invoke_result ^= Invoke(root_library, "myFunction");
  intptr_t result_int = Integer::Cast(invoke_result).Value();
  EXPECT_EQ(reinterpret_cast<intptr_t>(thread), result_int);
}

#if !defined(TARGET_ARCH_IA32)
ISOLATE_UNIT_TEST_CASE(IRTest_CachableIdempotentCall) {
  // clang-format off
  CStringUniquePtr kScript(OS::SCreate(nullptr, R"(
    int globalCounter = 0;

    int increment() => ++globalCounter;

    int cachedIncrement() {
      // We will replace this call with a cacheable call,
      // which will lead to the counter no longer being incremented.
      // Make sure to return the value, so we can see that the boxing and
      // unboxing works as expected.
      return increment();
    }

    @pragma("vm:entry-point", "call")
    int multipleIncrement() {
      int returnValue = 0;
      for(int i = 0; i < 10; i++) {
        // Save the last returned value.
        returnValue = cachedIncrement();
      }
      return returnValue;
    }
  )"));
  // clang-format on

  const auto& root_library = Library::Handle(LoadTestScript(kScript.get()));
  const auto& first_result =
      Object::Handle(Invoke(root_library, "multipleIncrement"));
  EXPECT(first_result.IsSmi());
  if (first_result.IsSmi()) {
    const intptr_t int_value = Smi::Cast(first_result).Value();
    EXPECT_EQ(10, int_value);
  }

  const auto& cached_increment_function =
      Function::Handle(GetFunction(root_library, "cachedIncrement"));

  const auto& increment_function =
      Function::ZoneHandle(GetFunction(root_library, "increment"));

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

  StaticCallInstr* static_call = nullptr;
  {
    ILMatcher cursor(flow_graph, flow_graph->graph_entry()->normal_entry());

    EXPECT(cursor.TryMatch({
        kMoveGlob,
        {kMatchAndMoveStaticCall, &static_call},
        kMoveGlob,
        kMatchDartReturn,
    }));
  }

  InputsArray args;
  CachableIdempotentCallInstr* call = new CachableIdempotentCallInstr(
      InstructionSource(), kUnboxedAddress, increment_function,
      static_call->type_args_len(), Array::empty_array(), std::move(args),
      DeoptId::kNone);
  static_call->ReplaceWith(call, nullptr);

  pipeline.RunForcedOptimizedAfterSSAPasses();

  {
    ILMatcher cursor(flow_graph, flow_graph->graph_entry()->normal_entry());

    EXPECT(cursor.TryMatch({
        kMoveGlob,
        kMatchAndMoveCachableIdempotentCall,
        kMoveGlob,
        // The cacheable call returns unboxed, so select representations
        // adds boxing.
        kMatchBox,
        kMoveGlob,
        kMatchDartReturn,
    }));
  }

  {
#if !defined(PRODUCT) && !defined(USING_THREAD_SANITIZER)
    SetFlagScope<bool> sfs(&FLAG_disassemble_optimized, true);
#endif
    pipeline.CompileGraphAndAttachFunction();
  }

  const auto& second_result =
      Object::Handle(Invoke(root_library, "multipleIncrement"));
  EXPECT(second_result.IsSmi());
  if (second_result.IsSmi()) {
    const intptr_t int_value = Smi::Cast(second_result).Value();
    EXPECT_EQ(11, int_value);
  }
}
#endif

// Helper to set up an inlined FfiCall by replacing a StaticCall.
FlowGraph* SetupFfiFlowgraph(TestPipeline* pipeline,
                             const compiler::ffi::CallMarshaller& marshaller,
                             uword native_entry,
                             bool is_leaf) {
  FlowGraph* flow_graph = pipeline->RunPasses({CompilerPass::kComputeSSA});

  {
    // Locate the placeholder call.
    StaticCallInstr* static_call = nullptr;
    {
      ILMatcher cursor(flow_graph, flow_graph->graph_entry()->normal_entry(),
                       /*trace=*/false);
      cursor.TryMatch({kMoveGlob, {kMatchStaticCall, &static_call}});
    }
    RELEASE_ASSERT(static_call != nullptr);

    // Store the native entry as an unboxed constant and convert it to an
    // untagged pointer for the FfiCall.
    Zone* const Z = flow_graph->zone();
    auto* const load_entry_point = new (Z) IntConverterInstr(
        kUnboxedIntPtr, kUntagged,
        new (Z) Value(flow_graph->GetConstant(
            Integer::Handle(Z, Integer::NewCanonical(native_entry)),
            kUnboxedIntPtr)));
    flow_graph->InsertBefore(static_call, load_entry_point, /*env=*/nullptr,
                             FlowGraph::kValue);

    // Make an FfiCall based on ffi_trampoline that calls our native function.
    const intptr_t num_arguments =
        FfiCallInstr::InputCountForMarshaller(marshaller);
    RELEASE_ASSERT(num_arguments == 1);
    InputsArray arguments(num_arguments);
    arguments.Add(new (Z) Value(load_entry_point));
    auto* const ffi_call = new (Z)
        FfiCallInstr(DeoptId::kNone, marshaller, is_leaf, std::move(arguments));
    RELEASE_ASSERT(
        ffi_call->InputAt(ffi_call->TargetAddressIndex())->definition() ==
        load_entry_point);
    flow_graph->InsertBefore(static_call, ffi_call, /*env=*/nullptr,
                             FlowGraph::kEffect);

    // Remove the placeholder call.
    static_call->RemoveFromGraph(/*return_previous=*/false);
  }

  // Run remaining relevant compiler passes.
  pipeline->RunAdditionalPasses({
      CompilerPass::kApplyICData,
      CompilerPass::kTryOptimizePatterns,
      CompilerPass::kSetOuterInliningId,
      CompilerPass::kTypePropagation,
      // Skipping passes that don't seem to do anything for this test.
      CompilerPass::kSelectRepresentations,
      // Skipping passes that don't seem to do anything for this test.
      CompilerPass::kTypePropagation,
      CompilerPass::kRangeAnalysis,
      // Skipping passes that don't seem to do anything for this test.
      CompilerPass::kFinalizeGraph,
      CompilerPass::kCanonicalize,
      CompilerPass::kAllocateRegisters,
      CompilerPass::kReorderBlocks,
  });

  return flow_graph;
}

// Test that FFI calls spill all live values to the stack, and that FFI leaf
// calls are free to use available ABI callee-save registers to avoid spilling.
// Additionally test that register allocation is done correctly by clobbering
// all volatile registers in the native function being called.
ISOLATE_UNIT_TEST_CASE(IRTest_FfiCallInstrLeafDoesntSpill) {
  const char* kScript = R"(
    import 'dart:ffi';

    // This is purely a placeholder and is never called.
    void placeholder() {}

    // Will call the "doFfiCall" and exercise its code.
    @pragma("vm:entry-point", "call")
    bool invokeDoFfiCall() {
      final double result = doFfiCall(1, 2, 3, 1.0, 2.0, 3.0);
      if (result != (2 + 3 + 4 + 2.0 + 3.0 + 4.0)) {
        throw 'Failed. Result was $result.';
      }
      return true;
    }

    // Will perform a "C" call while having live values in registers
    // across the FfiCall.
    double doFfiCall(int a, int b, int c, double x, double y, double z) {
      // Ensure there is at least one live value in a register.
      a += 1;
      b += 1;
      c += 1;
      x += 1.0;
      y += 1.0;
      z += 1.0;
      // We'll replace this StaticCall with an FfiCall.
      placeholder();
      // Use the live value.
      return (a + b + c + x + y + z);
    }

    // FFI trampoline function.
    typedef NT = Void Function();
    typedef DT = void Function();
    Pointer<NativeFunction<NT>> ptr = Pointer.fromAddress(0);
    @pragma("vm:entry-point", "call")
    DT getFfiTrampolineClosure() => ptr.asFunction(isLeaf:true);
  )";

  const auto& root_library = Library::Handle(LoadTestScript(kScript));

  // Build a "C" function that we can actually invoke.
  auto& c_function = Instructions::Handle(
      BuildInstructions([](compiler::Assembler* assembler) {
        // Clobber all volatile registers to make sure caller doesn't rely on
        // any non-callee-save register.
        for (intptr_t reg = 0; reg < kNumberOfFpuRegisters; reg++) {
          if ((kAbiVolatileFpuRegs & (1 << reg)) != 0) {
#if defined(TARGET_ARCH_ARM)
            // On ARM we need an extra scratch register for LoadDImmediate.
            assembler->LoadDImmediate(static_cast<DRegister>(reg), 0.0, R3);
#else
            assembler->LoadDImmediate(static_cast<FpuRegister>(reg), 0.0);
#endif
          }
        }
        for (intptr_t reg = 0; reg < kNumberOfCpuRegisters; reg++) {
          if ((kDartVolatileCpuRegs & (1 << reg)) != 0) {
            assembler->LoadImmediate(static_cast<Register>(reg), 0xDEADBEEF);
          }
        }
        assembler->Ret();
      }));
  uword native_entry = c_function.EntryPoint();

  // Get initial compilation done.
  Invoke(root_library, "invokeDoFfiCall");

  const Function& do_ffi_call =
      Function::Handle(GetFunction(root_library, "doFfiCall"));
  RELEASE_ASSERT(!do_ffi_call.IsNull());

  const auto& value = Closure::Handle(
      Closure::RawCast(Invoke(root_library, "getFfiTrampolineClosure")));
  RELEASE_ASSERT(value.IsClosure());
  const auto& ffi_trampoline =
      Function::ZoneHandle(Closure::Cast(value).function());
  RELEASE_ASSERT(!ffi_trampoline.IsNull());

  // Construct the FFICallInstr from the trampoline matching our native
  // function.
  const char* error = nullptr;
  auto* const zone = thread->zone();
  const auto& c_signature =
      FunctionType::ZoneHandle(zone, ffi_trampoline.FfiCSignature());
  const auto marshaller_ptr = compiler::ffi::CallMarshaller::FromFunction(
      zone, ffi_trampoline, /*function_params_start_at=*/1, c_signature,
      &error);
  RELEASE_ASSERT(error == nullptr);
  RELEASE_ASSERT(marshaller_ptr != nullptr);
  const auto& marshaller = *marshaller_ptr;

  const auto& compile_and_run =
      [&](bool is_leaf, std::function<void(ParallelMoveInstr*)> verify) {
        // Build the SSA graph for "doFfiCall"
        TestPipeline pipeline(do_ffi_call, CompilerPass::kJIT);
        FlowGraph* flow_graph =
            SetupFfiFlowgraph(&pipeline, marshaller, native_entry, is_leaf);

        {
          ParallelMoveInstr* parallel_move = nullptr;
          ILMatcher cursor(flow_graph,
                           flow_graph->graph_entry()->normal_entry(),
                           /*trace=*/false);
          while (cursor.TryMatch(
              {kMoveGlob, {kMatchAndMoveParallelMove, &parallel_move}})) {
            verify(parallel_move);
          }
        }

        // Finish the compilation and attach code so we can run it.
        pipeline.CompileGraphAndAttachFunction();

        // Ensure we can successfully invoke the FFI call.
        auto& result = Object::Handle(Invoke(root_library, "invokeDoFfiCall"));
        RELEASE_ASSERT(result.IsBool());
        EXPECT(Bool::Cast(result).value());
      };

  intptr_t num_cpu_reg_to_stack_nonleaf = 0;
  intptr_t num_cpu_reg_to_stack_leaf = 0;
  intptr_t num_fpu_reg_to_stack_nonleaf = 0;
  intptr_t num_fpu_reg_to_stack_leaf = 0;

  // Test non-leaf spills live values.
  compile_and_run(/*is_leaf=*/false, [&](ParallelMoveInstr* parallel_move) {
    // TargetAddress is passed in register, live values are all spilled.
    for (int i = 0; i < parallel_move->NumMoves(); i++) {
      auto move = parallel_move->moves()[i];
      if (move->src_slot()->IsRegister() && move->dest_slot()->IsStackSlot()) {
        num_cpu_reg_to_stack_nonleaf++;
      } else if (move->src_slot()->IsFpuRegister() &&
                 move->dest_slot()->IsDoubleStackSlot()) {
        num_fpu_reg_to_stack_nonleaf++;
      }
    }
  });

  // Test leaf calls do not cause spills of live values.
  compile_and_run(/*is_leaf=*/true, [&](ParallelMoveInstr* parallel_move) {
    // TargetAddress is passed in registers, live values are not spilled and
    // remains in callee-save registers.
    for (int i = 0; i < parallel_move->NumMoves(); i++) {
      auto move = parallel_move->moves()[i];
      if (move->src_slot()->IsRegister() && move->dest_slot()->IsStackSlot()) {
        num_cpu_reg_to_stack_leaf++;
      } else if (move->src_slot()->IsFpuRegister() &&
                 move->dest_slot()->IsDoubleStackSlot()) {
        num_fpu_reg_to_stack_leaf++;
      }
    }
  });

  // We should have less moves to the stack (i.e. spilling) in leaf calls.
  EXPECT_LT(num_cpu_reg_to_stack_leaf, num_cpu_reg_to_stack_nonleaf);
  // We don't have volatile FPU registers on all platforms.
  const bool has_callee_save_fpu_regs =
      Utils::CountOneBitsWord(kAbiVolatileFpuRegs) <
      Utils::CountOneBitsWord(kAllFpuRegistersList);
  EXPECT(!has_callee_save_fpu_regs ||
         num_fpu_reg_to_stack_leaf < num_fpu_reg_to_stack_nonleaf);
}

static void TestConstantFoldToSmi(const Library& root_library,
                                  const char* function_name,
                                  CompilerPass::PipelineMode mode,
                                  intptr_t expected_value) {
  const auto& function =
      Function::Handle(GetFunction(root_library, function_name));

  TestPipeline pipeline(function, mode);
  FlowGraph* flow_graph = pipeline.RunPasses({});

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

  DartReturnInstr* ret = nullptr;

  ILMatcher cursor(flow_graph, entry, true, ParallelMovesHandling::kSkip);
  RELEASE_ASSERT(cursor.TryMatch({
      kMoveGlob,
      {kMatchDartReturn, &ret},
  }));

  ConstantInstr* constant = ret->value()->definition()->AsConstant();
  EXPECT(constant != nullptr);
  if (constant != nullptr) {
    const Object& value = constant->value();
    EXPECT(value.IsSmi());
    if (value.IsSmi()) {
      const intptr_t int_value = Smi::Cast(value).Value();
      EXPECT_EQ(expected_value, int_value);
    }
  }
}

ISOLATE_UNIT_TEST_CASE(ConstantFold_bitLength) {
  // clang-format off
  auto kScript = R"(
      b0() => 0. bitLength;  // 0...00000
      b1() => 1. bitLength;  // 0...00001
      b100() => 100. bitLength;
      b200() => 200. bitLength;
      bffff() => 0xffff. bitLength;
      m1() => (-1).bitLength;  // 1...11111
      m2() => (-2).bitLength;  // 1...11110

      main() {
        b0();
        b1();
        b100();
        b200();
        bffff();
        m1();
        m2();
      }
    )";
  // clang-format on

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

  auto test = [&](const char* function, intptr_t expected) {
    TestConstantFoldToSmi(root_library, function, CompilerPass::kJIT, expected);
    TestConstantFoldToSmi(root_library, function, CompilerPass::kAOT, expected);
  };

  test("b0", 0);
  test("b1", 1);
  test("b100", 7);
  test("b200", 8);
  test("bffff", 16);
  test("m1", 0);
  test("m2", 1);
}

static void TestRepresentationChangeDuringCanonicalization(
    Thread* thread,
    bool allow_representation_change) {
  using compiler::BlockBuilder;

  const auto& lib = Library::Handle(Library::CoreLibrary());
  const Class& list_class =
      Class::Handle(lib.LookupClassAllowPrivate(Symbols::_List()));
  EXPECT(!list_class.IsNull());
  const Error& err = Error::Handle(list_class.EnsureIsFinalized(thread));
  EXPECT(err.IsNull());
  const Function& list_filled = Function::ZoneHandle(
      list_class.LookupFactoryAllowPrivate(Symbols::_ListFilledFactory()));
  EXPECT(!list_filled.IsNull());

  CompilerState S(thread, /*is_aot=*/true, /*is_optimizing=*/true);

  FlowGraphBuilderHelper H(/*num_parameters=*/1);
  H.AddVariable("param", AbstractType::ZoneHandle(Type::IntType()));

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();

  Definition* param = nullptr;
  LoadFieldInstr* load = nullptr;
  UnboxInstr* unbox = nullptr;
  Definition* add = nullptr;
  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    param = builder.AddParameter(0, kUnboxedInt64);

    InputsArray args;
    args.Add(new Value(H.flow_graph()->constant_null()));
    args.Add(new Value(param));
    args.Add(new Value(H.IntConstant(0)));
    StaticCallInstr* array = builder.AddDefinition(new StaticCallInstr(
        InstructionSource(), list_filled, 1, Array::empty_array(),
        std::move(args), DeoptId::kNone, 0, ICData::kNoRebind));
    array->UpdateType(CompileType::FromCid(kArrayCid));
    array->SetResultType(thread->zone(), CompileType::FromCid(kArrayCid));
    array->set_is_known_list_constructor(true);

    load = builder.AddDefinition(new LoadFieldInstr(
        new Value(array), Slot::Array_length(), InstructionSource()));

    unbox = builder.AddDefinition(new UnboxInt64Instr(
        new Value(load), DeoptId::kNone, UnboxInstr::ValueMode::kHasValidType));

    add = builder.AddDefinition(new BinaryInt64OpInstr(
        Token::kADD, new Value(unbox), new Value(H.IntConstant(1)),
        S.GetNextDeoptId()));

    Definition* box = builder.AddDefinition(new BoxInt64Instr(new Value(add)));

    builder.AddReturn(new Value(box));
  }

  H.FinishGraph();

  if (!allow_representation_change) {
    H.flow_graph()->disallow_unmatched_representations();
  }

  H.flow_graph()->Canonicalize();

  if (allow_representation_change) {
    EXPECT(add->InputAt(0)->definition() == param);
  } else {
    EXPECT(add->InputAt(0)->definition() == unbox);
    EXPECT(unbox->value()->definition() == load);
  }
}

ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_RepresentationChange) {
  TestRepresentationChangeDuringCanonicalization(thread, true);
  TestRepresentationChangeDuringCanonicalization(thread, false);
}

enum TypeDataField {
  TypedDataBase_length,
  TypedDataView_offset_in_bytes,
  TypedDataView_typed_data,
};

static void TestCanonicalizationOfTypedDataViewFieldLoads(
    Thread* thread,
    TypeDataField field_kind) {
  const auto& typed_data_lib = Library::Handle(Library::TypedDataLibrary());
  const auto& view_cls = Class::Handle(
      typed_data_lib.LookupClassAllowPrivate(Symbols::_Float32ArrayView()));
  const Error& err = Error::Handle(view_cls.EnsureIsFinalized(thread));
  EXPECT(err.IsNull());
  const auto& factory =
      Function::ZoneHandle(view_cls.LookupFactoryAllowPrivate(String::Handle(
          String::Concat(Symbols::_Float32ArrayView(), Symbols::DotUnder()))));
  EXPECT(!factory.IsNull());

  using compiler::BlockBuilder;
  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);
  FlowGraphBuilderHelper H;

  const Slot* field = nullptr;
  switch (field_kind) {
    case TypedDataBase_length:
      field = &Slot::TypedDataBase_length();
      break;
    case TypedDataView_offset_in_bytes:
      field = &Slot::TypedDataView_offset_in_bytes();
      break;
    case TypedDataView_typed_data:
      field = &Slot::TypedDataView_typed_data();
      break;
  }

  auto b1 = H.flow_graph()->graph_entry()->normal_entry();

  const auto constant_4 = H.IntConstant(4);
  const auto constant_1 = H.IntConstant(1);

  Definition* array;
  Definition* load;
  DartReturnInstr* ret;

  {
    BlockBuilder builder(H.flow_graph(), b1);
    // array <- AllocateTypedData(1)
    array = builder.AddDefinition(new AllocateTypedDataInstr(
        InstructionSource(), kTypedDataFloat64ArrayCid, new Value(constant_1),
        DeoptId::kNone));
    // view <- StaticCall(_Float32ArrayView._, null, array, 4, 1)
    const auto view = builder.AddDefinition(new StaticCallInstr(
        InstructionSource(), factory, 1, Array::empty_array(),
        {new Value(H.flow_graph()->constant_null()), new Value(array),
         new Value(constant_4), new Value(constant_1)},
        DeoptId::kNone, 1, ICData::RebindRule::kStatic));
    // array_alias <- LoadField(view.length)
    load = builder.AddDefinition(
        new LoadFieldInstr(new Value(view), *field, InstructionSource()));
    // Return(load)
    ret = builder.AddReturn(new Value(load));
  }
  H.FinishGraph();
  H.flow_graph()->Canonicalize();

  switch (field_kind) {
    case TypedDataBase_length:
      EXPECT_PROPERTY(ret->value()->definition(), &it == constant_1);
      break;
    case TypedDataView_offset_in_bytes:
      EXPECT_PROPERTY(ret->value()->definition(), &it == constant_4);
      break;
    case TypedDataView_typed_data:
      EXPECT_PROPERTY(ret->value()->definition(), &it == array);
      break;
  }
}

ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_TypedDataViewFactory) {
  TestCanonicalizationOfTypedDataViewFieldLoads(thread, TypedDataBase_length);
  TestCanonicalizationOfTypedDataViewFieldLoads(thread,
                                                TypedDataView_offset_in_bytes);
  TestCanonicalizationOfTypedDataViewFieldLoads(thread,
                                                TypedDataView_typed_data);
}

// Check that canonicalize can devirtualize InstanceCall based on type
// information in AOT mode.
ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_InstanceCallWithNoICDataInAOT) {
  const auto& typed_data_lib = Library::Handle(Library::TypedDataLibrary());
  const auto& view_cls = Class::Handle(typed_data_lib.LookupClassAllowPrivate(
      String::Handle(Symbols::New(thread, "_TypedListBase"))));
  const Error& err = Error::Handle(view_cls.EnsureIsFinalized(thread));
  EXPECT(err.IsNull());
  const auto& getter = Function::Handle(
      view_cls.LookupFunctionAllowPrivate(Symbols::GetLength()));
  EXPECT(!getter.IsNull());

  using compiler::BlockBuilder;
  CompilerState S(thread, /*is_aot=*/true, /*is_optimizing=*/true);
  FlowGraphBuilderHelper H;

  auto b1 = H.flow_graph()->graph_entry()->normal_entry();

  InstanceCallInstr* length_call;
  DartReturnInstr* ret;

  {
    BlockBuilder builder(H.flow_graph(), b1);
    // array <- AllocateTypedData(1)
    const auto array = builder.AddDefinition(new AllocateTypedDataInstr(
        InstructionSource(), kTypedDataFloat64ArrayCid,
        new Value(H.IntConstant(1)), DeoptId::kNone));
    // length_call <- InstanceCall('get:length', array, ICData[])
    length_call = builder.AddDefinition(new InstanceCallInstr(
        InstructionSource(), Symbols::GetLength(), Token::kGET,
        /*arguments=*/{new Value(array)}, 0, Array::empty_array(), 1,
        /*deopt_id=*/42));
    length_call->EnsureICData(H.flow_graph());
    // Return(load)
    ret = builder.AddReturn(new Value(length_call));
  }
  H.FinishGraph();
  H.flow_graph()->Canonicalize();

  EXPECT_PROPERTY(length_call, it.previous() == nullptr);
  EXPECT_PROPERTY(ret->value()->definition(), it.IsStaticCall());
  EXPECT_PROPERTY(ret->value()->definition()->AsStaticCall(),
                  it.function().ptr() == getter.ptr());
}

static void TestTestRangeCanonicalize(const AbstractType& type,
                                      uword lower,
                                      uword upper,
                                      bool result) {
  using compiler::BlockBuilder;
  CompilerState S(Thread::Current(), /*is_aot=*/true, /*is_optimizing=*/true);
  FlowGraphBuilderHelper H(/*num_parameters=*/1);
  H.AddVariable("v0", type);

  auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();

  DartReturnInstr* ret;
  {
    BlockBuilder builder(H.flow_graph(), normal_entry);
    Definition* param = builder.AddParameter(0, kTagged);
    Definition* load_cid =
        builder.AddDefinition(new LoadClassIdInstr(new Value(param)));
    Definition* test_range = builder.AddDefinition(new TestRangeInstr(
        InstructionSource(), new Value(load_cid), lower, upper, kTagged));
    ret = builder.AddReturn(new Value(test_range));
  }
  H.FinishGraph();
  H.flow_graph()->Canonicalize();

  EXPECT_PROPERTY(ret, it.value()->BindsToConstant());
  EXPECT_PROPERTY(ret,
                  it.value()->BoundConstant().ptr() == Bool::Get(result).ptr());
}

ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_TestRange) {
  HierarchyInfo hierarchy_info(thread);
  TestTestRangeCanonicalize(AbstractType::ZoneHandle(Type::IntType()),
                            kOneByteStringCid, kTwoByteStringCid, false);
  TestTestRangeCanonicalize(AbstractType::ZoneHandle(Type::IntType()), kSmiCid,
                            kMintCid, true);
  TestTestRangeCanonicalize(AbstractType::ZoneHandle(Type::NullType()), kSmiCid,
                            kMintCid, false);
  TestTestRangeCanonicalize(AbstractType::ZoneHandle(Type::Double()), kSmiCid,
                            kMintCid, false);
  TestTestRangeCanonicalize(AbstractType::ZoneHandle(Type::ObjectType()), 1,
                            kClassIdTagMax, true);
}

void TestStaticFieldForwarding(Thread* thread,
                               const Class& test_cls,
                               const Field& field,
                               intptr_t num_stores,
                               bool expected_to_forward) {
  EXPECT(num_stores <= 2);

  using compiler::BlockBuilder;
  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);
  FlowGraphBuilderHelper H;

  auto b1 = H.flow_graph()->graph_entry()->normal_entry();

  const auto constant_42 = H.IntConstant(42);
  const auto constant_24 = H.IntConstant(24);
  Definition* load;
  DartReturnInstr* ret;

  {
    BlockBuilder builder(H.flow_graph(), b1);
    // obj <- AllocateObject(TestClass)
    const auto obj = builder.AddDefinition(
        new AllocateObjectInstr(InstructionSource(), test_cls, DeoptId::kNone));

    if (num_stores >= 1) {
      // StoreField(o.field = 42)
      builder.AddInstruction(new StoreFieldInstr(
          field, new Value(obj), new Value(constant_42),
          StoreBarrierType::kNoStoreBarrier, InstructionSource(),
          &H.flow_graph()->parsed_function(),
          StoreFieldInstr::Kind::kInitializing));
    }

    if (num_stores >= 2) {
      // StoreField(o.field = 24)
      builder.AddInstruction(new StoreFieldInstr(
          field, new Value(obj), new Value(constant_24),
          StoreBarrierType::kNoStoreBarrier, InstructionSource(),
          &H.flow_graph()->parsed_function()));
    }

    // load <- LoadField(view.field)
    load = builder.AddDefinition(new LoadFieldInstr(
        new Value(obj), Slot::Get(field, &H.flow_graph()->parsed_function()),
        InstructionSource()));

    // Return(load)
    ret = builder.AddReturn(new Value(load));
  }
  H.FinishGraph();
  H.flow_graph()->Canonicalize();

  if (expected_to_forward) {
    EXPECT_PROPERTY(ret->value()->definition(), &it == constant_42);
  } else {
    EXPECT_PROPERTY(ret->value()->definition(), &it == load);
  }
}

ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_FinalFieldForwarding) {
  const char* script_chars = R"(
    import 'dart:typed_data';

    class TestClass {
      final dynamic finalField;
      late final dynamic lateFinalField;
      dynamic normalField;

      TestClass(this.finalField, this.lateFinalField, this.normalField);
    }
  )";
  const auto& lib = Library::Handle(LoadTestScript(script_chars));

  const auto& test_cls = Class::ZoneHandle(
      lib.LookupClass(String::Handle(Symbols::New(thread, "TestClass"))));
  const auto& err = Error::Handle(test_cls.EnsureIsFinalized(thread));
  EXPECT(err.IsNull());

  const auto lookup_field = [&](const char* name) -> const Field& {
    const auto& original_field = Field::Handle(
        test_cls.LookupField(String::Handle(Symbols::New(thread, name))));
    EXPECT(!original_field.IsNull());
    return Field::Handle(original_field.CloneFromOriginal());
  };

  const auto& final_field = lookup_field("finalField");
  const auto& late_final_field = lookup_field("lateFinalField");
  const auto& normal_field = lookup_field("normalField");

  TestStaticFieldForwarding(thread, test_cls, final_field, /*num_stores=*/0,
                            /*expected_to_forward=*/false);
  TestStaticFieldForwarding(thread, test_cls, final_field, /*num_stores=*/1,
                            /*expected_to_forward=*/true);
  TestStaticFieldForwarding(thread, test_cls, final_field, /*num_stores=*/2,
                            /*expected_to_forward=*/false);

  TestStaticFieldForwarding(thread, test_cls, late_final_field,
                            /*num_stores=*/0, /*expected_to_forward=*/false);
  TestStaticFieldForwarding(thread, test_cls, late_final_field,
                            /*num_stores=*/1, /*expected_to_forward=*/false);
  TestStaticFieldForwarding(thread, test_cls, late_final_field,
                            /*num_stores=*/2, /*expected_to_forward=*/false);

  TestStaticFieldForwarding(thread, test_cls, normal_field, /*num_stores=*/0,
                            /*expected_to_forward=*/false);
  TestStaticFieldForwarding(thread, test_cls, normal_field, /*num_stores=*/1,
                            /*expected_to_forward=*/false);
  TestStaticFieldForwarding(thread, test_cls, normal_field, /*num_stores=*/2,
                            /*expected_to_forward=*/false);
}

void TestBoxIntegerUnboxedConstantCanonicalization(Thread* thread,
                                                   int64_t value,
                                                   Representation constant_rep,
                                                   Representation from_rep,
                                                   bool should_canonicalize) {
  using compiler::BlockBuilder;
  CompilerState S(thread, /*is_aot=*/false, /*is_optimizing=*/true);
  FlowGraphBuilderHelper H;

  auto b1 = H.flow_graph()->graph_entry()->normal_entry();

  auto* const unboxed_constant = H.IntConstant(value, constant_rep);
  auto* const boxed_constant = H.IntConstant(value);

  BoxInstr* box;
  DartReturnInstr* ret;

  {
    BlockBuilder builder(H.flow_graph(), b1);
    box = builder.AddDefinition(
        BoxInstr::Create(from_rep, new Value(unboxed_constant)));

    ret = builder.AddReturn(new Value(box));
  }
  H.FinishGraph();
  H.flow_graph()->Canonicalize();

  if (should_canonicalize) {
    EXPECT_PROPERTY(ret->value()->definition(), &it == boxed_constant);
    EXPECT_PROPERTY(box, !it.HasUses());
  } else {
    EXPECT_PROPERTY(ret->value()->definition(), &it == box);
  }
}

// Check that canonicalize can replace BoxInteger<from>(UnboxedConstant<to>(v))
// with v if v is representable in from, and does not if it is not.
ISOLATE_UNIT_TEST_CASE(IL_Canonicalize_BoxIntegerUnboxedConstant) {
  // kUnboxedInt8
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 0, kUnboxedInt8,
                                                kUnboxedInt8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 1, kUnboxedInt8,
                                                kUnboxedInt8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, -1, kUnboxedInt8,
                                                kUnboxedInt8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt8, kUnboxedInt8,
                                                kUnboxedInt8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt8, kUnboxedInt8,
                                                kUnboxedInt8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint8,
                                                kUnboxedUint8, kUnboxedInt8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt32,
                                                kUnboxedInt32, kUnboxedInt8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt32,
                                                kUnboxedInt32, kUnboxedInt8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedUint32, kUnboxedInt8,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMinInt32) - 1, kUnboxedInt32, kUnboxedInt8,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint32,
                                                kUnboxedUint32, kUnboxedInt8,
                                                /*should_canonicalize=*/false);

  // kUnboxedUint8
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 0, kUnboxedInt8,
                                                kUnboxedUint8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 1, kUnboxedInt8,
                                                kUnboxedUint8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, -1, kUnboxedInt8,
                                                kUnboxedUint8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt8, kUnboxedInt8,
                                                kUnboxedUint8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt8, kUnboxedInt8,
                                                kUnboxedUint8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint8,
                                                kUnboxedUint8, kUnboxedUint8,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt32,
                                                kUnboxedInt32, kUnboxedUint8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt32,
                                                kUnboxedInt32, kUnboxedUint8,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedUint32,
      kUnboxedUint8,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMinInt32) - 1, kUnboxedInt32, kUnboxedUint8,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint32,
                                                kUnboxedUint32, kUnboxedUint8,
                                                /*should_canonicalize=*/false);

  // kUnboxedInt32
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 0, kUnboxedInt8,
                                                kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 1, kUnboxedInt8,
                                                kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, -1, kUnboxedInt8,
                                                kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt8, kUnboxedInt8,
                                                kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt8, kUnboxedInt8,
                                                kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint8,
                                                kUnboxedUint8, kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt32,
                                                kUnboxedInt32, kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt32,
                                                kUnboxedInt32, kUnboxedInt32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedUint32,
      kUnboxedInt32,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMinInt32) - 1, kUnboxedInt32, kUnboxedInt32,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint32,
                                                kUnboxedUint32, kUnboxedInt32,
                                                /*should_canonicalize=*/false);

  // kUnboxedUint32
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 0, kUnboxedInt8,
                                                kUnboxedUint32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 1, kUnboxedInt8,
                                                kUnboxedUint32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, -1, kUnboxedInt8,
                                                kUnboxedUint32,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt8, kUnboxedInt8,
                                                kUnboxedUint32,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt8, kUnboxedInt8,
                                                kUnboxedUint32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint8,
                                                kUnboxedUint8, kUnboxedUint32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt32,
                                                kUnboxedInt32, kUnboxedUint32,
                                                /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt32,
                                                kUnboxedInt32, kUnboxedUint32,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedInt32,
      kUnboxedUint32,
      /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMinInt32) - 1, kUnboxedInt32,
      kUnboxedUint32,
      /*should_canonicalize=*/false);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint32,
                                                kUnboxedUint32, kUnboxedUint32,
                                                /*should_canonicalize=*/true);

  // kUnboxedInt64
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 0, kUnboxedInt8,
                                                kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, 1, kUnboxedInt8,
                                                kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, -1, kUnboxedInt8,
                                                kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt8, kUnboxedInt8,
                                                kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt8, kUnboxedInt8,
                                                kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint8,
                                                kUnboxedUint8, kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMinInt32,
                                                kUnboxedInt32, kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxInt32,
                                                kUnboxedInt32, kUnboxedInt64,
                                                /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMaxInt32) + 1, kUnboxedInt32, kUnboxedInt64,
      /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(
      thread, static_cast<int64_t>(kMinInt32) - 1, kUnboxedInt32, kUnboxedInt64,
      /*should_canonicalize=*/true);
  TestBoxIntegerUnboxedConstantCanonicalization(thread, kMaxUint32,
                                                kUnboxedUint32, kUnboxedInt64,
                                                /*should_canonicalize=*/true);
}

template <typename... Args>
static ObjectPtr InvokeFunction(const Function& function, Args&... args) {
  const Array& args_array = Array::Handle(Array::New(sizeof...(Args)));
  intptr_t i = 0;
  (args_array.SetAt(i++, args), ...);
  return DartEntry::InvokeFunction(function, args_array);
}

static const Function& BuildTestFunction(
    intptr_t num_parameters,
    std::function<void(FlowGraphBuilderHelper&)> build_graph) {
  using compiler::BlockBuilder;

  TestPipeline pipeline(CompilerPass::kAOT, [&]() {
    FlowGraphBuilderHelper H(num_parameters);
    build_graph(H);
    H.FinishGraph();
    return H.flow_graph();
  });
  auto flow_graph = pipeline.RunPasses({
      CompilerPass::kFinalizeGraph,
      CompilerPass::kReorderBlocks,
      CompilerPass::kAllocateRegisters,
  });
  pipeline.CompileGraphAndAttachFunction();
  return flow_graph->function();
}

enum class TestIntVariant {
  kTestBranch,
  kTestValue,
};

static const Function& BuildTestIntFunction(
    Zone* zone,
    TestIntVariant test_variant,
    bool eq_zero,
    Representation rep,
    std::optional<int64_t> immediate_mask) {
  using compiler::BlockBuilder;
  return BuildTestFunction(
      /*num_parameters=*/1 + (!immediate_mask.has_value() ? 1 : 0),
      [&](auto& H) {
        H.AddVariable("lhs", AbstractType::ZoneHandle(Type::IntType()),
                      new CompileType(CompileType::Int()));
        if (!immediate_mask.has_value()) {
          H.AddVariable("rhs", AbstractType::ZoneHandle(Type::IntType()),
                        new CompileType(CompileType::Int()));
        }

        auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();
        auto true_successor = H.TargetEntry();
        auto false_successor = H.TargetEntry();

        {
          BlockBuilder builder(H.flow_graph(), normal_entry);
          Definition* lhs = builder.AddParameter(0);
          Definition* rhs = immediate_mask.has_value()
                                ? H.IntConstant(immediate_mask.value(), rep)
                                : builder.AddParameter(1);
          if (rep != lhs->representation()) {
            lhs = builder.AddUnboxInstr(kUnboxedInt64, lhs,
                                        UnboxInstr::ValueMode::kHasValidType);
          }
          if (rep != rhs->representation()) {
            rhs = builder.AddUnboxInstr(kUnboxedInt64, rhs,
                                        UnboxInstr::ValueMode::kHasValidType);
          }

          auto comparison = new TestIntInstr(
              InstructionSource(), eq_zero ? Token::kEQ : Token::kNE, rep,
              new Value(lhs), new Value(rhs));

          if (test_variant == TestIntVariant::kTestValue) {
            auto v2 = builder.AddDefinition(comparison);
            builder.AddReturn(new Value(v2));
          } else {
            builder.AddBranch(comparison, true_successor, false_successor);
          }
        }

        if (test_variant == TestIntVariant::kTestBranch) {
          {
            BlockBuilder builder(H.flow_graph(), true_successor);
            builder.AddReturn(
                new Value(H.flow_graph()->GetConstant(Bool::True())));
          }

          {
            BlockBuilder builder(H.flow_graph(), false_successor);
            builder.AddReturn(
                new Value(H.flow_graph()->GetConstant(Bool::False())));
          }
        }
      });
}

static void TestIntTestWithImmediate(Zone* zone,
                                     TestIntVariant test_variant,
                                     bool eq_zero,
                                     Representation rep,
                                     const std::vector<int64_t>& inputs,
                                     int64_t mask) {
  const auto& func =
      BuildTestIntFunction(zone, test_variant, eq_zero, rep, mask);
  auto invoke = [&](int64_t v) -> bool {
    const auto& input = Integer::Handle(Integer::New(v));
    EXPECT(rep == kUnboxedInt64 || input.IsSmi());
    const auto& result = Bool::CheckedHandle(zone, InvokeFunction(func, input));
    return result.value();
  };

  for (auto& input : inputs) {
    const auto expected = ((input & mask) == 0) == eq_zero;
    const auto got = invoke(input);
    if (expected != got) {
      FAIL("testing [%s] [%s] %" Px64 " & %" Px64
           " %s 0: expected %s but got %s\n",
           test_variant == TestIntVariant::kTestBranch ? "branch" : "value",
           RepresentationUtils::ToCString(rep), input, mask,
           eq_zero ? "==" : "!=", expected ? "true" : "false",
           got ? "true" : "false");
    }
  }
}

static void TestIntTest(Zone* zone,
                        TestIntVariant test_variant,
                        bool eq_zero,
                        Representation rep,
                        const std::vector<int64_t>& inputs,
                        const std::vector<int64_t>& masks) {
  if (!TestIntInstr::IsSupported(rep)) {
    return;
  }

  const auto& func = BuildTestIntFunction(zone, test_variant, eq_zero, rep, {});
  auto invoke = [&](int64_t lhs, int64_t mask) -> bool {
    const auto& arg0 = Integer::Handle(Integer::New(lhs));
    const auto& arg1 = Integer::Handle(Integer::New(mask));
    EXPECT(rep == kUnboxedInt64 || arg0.IsSmi());
    EXPECT(rep == kUnboxedInt64 || arg1.IsSmi());
    const auto& result =
        Bool::CheckedHandle(zone, InvokeFunction(func, arg0, arg1));
    return result.value();
  };

  for (auto& mask : masks) {
    TestIntTestWithImmediate(zone, test_variant, eq_zero, rep, inputs, mask);

    // We allow non-Smi masks as immediates but not as non-constant operands.
    if (rep == kTagged && !Smi::IsValid(mask)) {
      continue;
    }

    for (auto& input : inputs) {
      const auto expected = ((input & mask) == 0) == eq_zero;
      const auto got = invoke(input, mask);
      if (expected != got) {
        FAIL("testing [%s] [%s] %" Px64 " & %" Px64
             " %s 0: expected %s but got %s\n",
             test_variant == TestIntVariant::kTestBranch ? "branch" : "value",
             RepresentationUtils::ToCString(rep), input, mask,
             eq_zero ? "==" : "!=", expected ? "true" : "false",
             got ? "true" : "false");
      }
    }
  }
}

ISOLATE_UNIT_TEST_CASE(IL_TestIntInstr) {
  const int64_t msb = static_cast<int64_t>(0x8000000000000000L);
  const int64_t kSmiSignBit = kSmiMax + 1;

  const std::initializer_list<int64_t> kMasks = {
      1, 2, kSmiSignBit, kSmiSignBit | 1, msb, msb | 1};

  const std::vector<std::pair<Representation, std::vector<int64_t>>> kValues = {
      {kTagged,
       {-2, -1, 0, 1, 2, 3, kSmiMax & ~1, kSmiMin & ~1, kSmiMax | 1,
        kSmiMin | 1}},
      {kUnboxedInt64,
       {-2, -1, 0, 1, 2, 3, kSmiMax & ~1, kSmiMin & ~1, kSmiMax | 1,
        kSmiMin | 1, msb, msb | 1, msb | 2}},
  };

  for (auto test_variant :
       {TestIntVariant::kTestBranch, TestIntVariant::kTestValue}) {
    for (auto eq_zero : {true, false}) {
      for (auto& [rep, values] : kValues) {
        TestIntTest(thread->zone(), test_variant, eq_zero, rep, values, kMasks);
      }
    }
  }
}

// This is a smoke test which verifies that RecordCoverage instruction is not
// accidentally removed by some overly eager optimization.
ISOLATE_UNIT_TEST_CASE(IL_RecordCoverageSurvivesOptimizations) {
  using compiler::BlockBuilder;
  SetFlagScope<bool> sfs(&FLAG_reorder_basic_blocks, false);

  TestPipeline pipeline(CompilerPass::kJIT, [&]() {
    FlowGraphBuilderHelper H(/*num_parameters=*/0);

    {
      BlockBuilder builder(H.flow_graph(),
                           H.flow_graph()->graph_entry()->normal_entry());
      const auto& coverage_array =
          TypedData::Handle(TypedData::New(kTypedDataUint32ArrayCid, 1));
      coverage_array.SetUint32(0, 0);
      builder.AddInstruction(
          new RecordCoverageInstr(coverage_array, 0, InstructionSource()));
      builder.AddReturn(new Value(H.flow_graph()->constant_null()));
    }

    H.FinishGraph();
    return H.flow_graph();
  });

  auto flow_graph = pipeline.RunPasses({});

  // RecordCoverage instruction should remain in the graph.
  EXPECT(flow_graph->graph_entry()->normal_entry()->next()->IsRecordCoverage());
}

// This test verifies that the ASSERT in Assembler::ElementAddressForIntIndex
// appropriately accounts for the heap object tag to check the displacement.
// Regression test for https://github.com/dart-lang/sdk/issues/56588.
ISOLATE_UNIT_TEST_CASE(IRTest_Regress_56588) {
  TestPipeline pipeline(CompilerPass::kAOT, [&]() {
    FlowGraphBuilderHelper H(1);

    const classid_t cid = kTypedDataUint8ClampedArrayCid;
    // Must not be a view or an external cid, so that the untagged address
    // isn't extracted first.
    EXPECT(IsTypedDataClassId(cid));
    const intptr_t index_scale = 1;
    // Set the constant index such that the displacement is not a signed 32-bit
    // integer unless the displacement also takes into account that the
    // base address is tagged.
    const int64_t constant_index =
        static_cast<int64_t>(kMaxInt32) + kHeapObjectTag -
        compiler::target::Instance::DataOffsetFor(cid);

    // Double-check that we chose such an index correctly.
    const int64_t disp = constant_index * index_scale +
                         compiler::target::Instance::DataOffsetFor(cid);
    EXPECT(!Utils::IsInt(32, disp));
    EXPECT(Utils::IsInt(32, disp - kHeapObjectTag));

    const auto& cls =
        Class::Handle(IsolateGroup::Current()->class_table()->At(cid));
    ASSERT(!cls.IsNull());
    auto& type =
        AbstractType::ZoneHandle(Type::New(cls, Object::null_type_arguments()));
    type = ClassFinalizer::FinalizeType(type);

    H.AddVariable("arg", type);

    auto normal_entry = H.flow_graph()->graph_entry()->normal_entry();

    {
      compiler::BlockBuilder builder(H.flow_graph(), normal_entry);
      Definition* const array = builder.AddParameter(0);
      auto* const index = H.IntConstant(constant_index, kUnboxedInt64);
      auto* const deref = builder.AddDefinition(new LoadIndexedInstr(
          new Value(array), new Value(index),
          index->representation() != kTagged, index_scale, cid, kAlignedAccess,
          CompilerState::Current().GetNextDeoptId(), InstructionSource()));
      builder.AddReturn(new Value(deref));
    }
    H.FinishGraph();
    return H.flow_graph();
  });
  pipeline.RunPasses({});
  pipeline.CompileGraphAndAttachFunction();
}

}  // namespace dart
