// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/interpreter/bytecodes.h"
#include "src/interpreter/interpreter.h"
#include "test/cctest/test-api.h"

namespace v8 {
namespace internal {

class IgnitionStatisticsTester {
 public:
  explicit IgnitionStatisticsTester(Isolate* isolate) : isolate_(isolate) {
    // In case the build specified v8_enable_ignition_dispatch_counting, the
    // interpreter already has a dispatch counters table and the bytecode
    // handlers will update it. To avoid crashes, we keep that array alive here.
    // This file doesn't test the results in the real array since there is no
    // automated testing on configurations with
    // v8_enable_ignition_dispatch_counting.
    original_bytecode_dispatch_counters_table_ =
        std::move(isolate->interpreter()->bytecode_dispatch_counters_table_);

    // This sets up the counters array, but does not rewrite the bytecode
    // handlers to update it.
    isolate->interpreter()->InitDispatchCounters();
  }

  void SetDispatchCounter(interpreter::Bytecode from, interpreter::Bytecode to,
                          uintptr_t value) const {
    int from_index = interpreter::Bytecodes::ToByte(from);
    int to_index = interpreter::Bytecodes::ToByte(to);
    isolate_->interpreter()->bytecode_dispatch_counters_table_
        [from_index * interpreter::Bytecodes::kBytecodeCount + to_index] =
        value;
    CHECK_EQ(isolate_->interpreter()->GetDispatchCounter(from, to), value);
  }

 private:
  Isolate* isolate_;
  std::unique_ptr<uintptr_t[]> original_bytecode_dispatch_counters_table_;
};

TEST(IgnitionStatisticsExtension) {
  FLAG_expose_ignition_statistics = true;
  CcTest::InitializeVM();
  v8::Isolate* isolate = CcTest::isolate();
  v8::HandleScope scope(isolate);
  IgnitionStatisticsTester tester(CcTest::i_isolate());

  Local<Value> typeof_result =
      CompileRun("typeof getIgnitionDispatchCounters === 'function'");
  CHECK(typeof_result->BooleanValue(isolate));

  // Get the list of all bytecode names into a JavaScript array.
#define BYTECODE_NAME_WITH_COMMA(Name, ...) "'" #Name "', "
  const char* kBytecodeNames =
      "var bytecodeNames = [" BYTECODE_LIST(BYTECODE_NAME_WITH_COMMA) "];";
#undef BYTECODE_NAME_WITH_COMMA
  CompileRun(kBytecodeNames);

  // Check that the dispatch counters object is a non-empty object of objects
  // where each property name is a bytecode name, in order, and each inner
  // object is empty.
  const char* kEmptyTest = R"(
    var emptyCounters = getIgnitionDispatchCounters();
    function isEmptyDispatchCounters(counters) {
      if (typeof counters !== "object") return false;
      var i = 0;
      for (var sourceBytecode in counters) {
        if (sourceBytecode !== bytecodeNames[i]) return false;
        var countersRow = counters[sourceBytecode];
        if (typeof countersRow !== "object") return false;
        for (var counter in countersRow) {
          return false;
        }
        ++i;
      }
      return true;
    }
    isEmptyDispatchCounters(emptyCounters);)";
  Local<Value> empty_result = CompileRun(kEmptyTest);
  CHECK(empty_result->BooleanValue(isolate));

  // Simulate running some code, which would update the counters.
  tester.SetDispatchCounter(interpreter::Bytecode::kLdar,
                            interpreter::Bytecode::kStar, 3);
  tester.SetDispatchCounter(interpreter::Bytecode::kLdar,
                            interpreter::Bytecode::kLdar, 4);
  tester.SetDispatchCounter(interpreter::Bytecode::kMov,
                            interpreter::Bytecode::kLdar, 5);

  // Check that the dispatch counters object is a non-empty object of objects
  // where each property name is a bytecode name, in order, and the inner
  // objects reflect the new state.
  const char* kNonEmptyTest = R"(
    var nonEmptyCounters = getIgnitionDispatchCounters();
    function isUpdatedDispatchCounters(counters) {
      if (typeof counters !== "object") return false;
      var i = 0;
      for (var sourceBytecode in counters) {
        if (sourceBytecode !== bytecodeNames[i]) return false;
        var countersRow = counters[sourceBytecode];
        if (typeof countersRow !== "object") return false;
        switch (sourceBytecode) {
          case "Ldar":
            if (JSON.stringify(countersRow) !== '{"Ldar":4,"Star":3}')
              return false;
            break;
          case "Mov":
            if (JSON.stringify(countersRow) !== '{"Ldar":5}')
              return false;
            break;
          default:
            for (var counter in countersRow) {
              return false;
            }
        }
        ++i;
      }
      return true;
    }
    isUpdatedDispatchCounters(nonEmptyCounters);)";
  Local<Value> non_empty_result = CompileRun(kNonEmptyTest);
  CHECK(non_empty_result->BooleanValue(isolate));
}

}  // namespace internal
}  // namespace v8
