// Copyright 2017 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/wasm/wasm-code-specialization.h"

#include "src/assembler-inl.h"
#include "src/objects-inl.h"
#include "src/source-position-table.h"
#include "src/wasm/decoder.h"
#include "src/wasm/wasm-module.h"
#include "src/wasm/wasm-opcodes.h"

using namespace v8::internal;
using namespace v8::internal::wasm;

namespace {

int ExtractDirectCallIndex(wasm::Decoder& decoder, const byte* pc) {
  DCHECK_EQ(static_cast<int>(kExprCallFunction), static_cast<int>(*pc));
  decoder.Reset(pc + 1, pc + 6);
  uint32_t call_idx = decoder.consume_u32v("call index");
  DCHECK(decoder.ok());
  DCHECK_GE(kMaxInt, call_idx);
  return static_cast<int>(call_idx);
}

int AdvanceSourcePositionTableIterator(SourcePositionTableIterator& iterator,
                                       size_t offset_l) {
  DCHECK_GE(kMaxInt, offset_l);
  int offset = static_cast<int>(offset_l);
  DCHECK(!iterator.done());
  int byte_pos;
  do {
    byte_pos = iterator.source_position().ScriptOffset();
    iterator.Advance();
  } while (!iterator.done() && iterator.code_offset() <= offset);
  return byte_pos;
}

class PatchDirectCallsHelper {
 public:
  PatchDirectCallsHelper(WasmInstanceObject* instance, Code* code)
      : source_pos_it(code->SourcePositionTable()), decoder(nullptr, nullptr) {
    FixedArray* deopt_data = code->deoptimization_data();
    DCHECK_EQ(2, deopt_data->length());
    WasmCompiledModule* comp_mod = instance->compiled_module();
    int func_index = Smi::ToInt(deopt_data->get(1));
    func_bytes = comp_mod->module_bytes()->GetChars() +
                 comp_mod->module()->functions[func_index].code.offset();
  }

  SourcePositionTableIterator source_pos_it;
  Decoder decoder;
  const byte* func_bytes;
};

bool IsAtWasmDirectCallTarget(RelocIterator& it) {
  DCHECK(RelocInfo::IsCodeTarget(it.rinfo()->rmode()));
  Code* code = Code::GetCodeFromTargetAddress(it.rinfo()->target_address());
  return code->kind() == Code::WASM_FUNCTION ||
         code->kind() == Code::WASM_TO_JS_FUNCTION ||
         code->kind() == Code::WASM_INTERPRETER_ENTRY ||
         code->builtin_index() == Builtins::kIllegal ||
         code->builtin_index() == Builtins::kWasmCompileLazy;
}

}  // namespace

CodeSpecialization::CodeSpecialization(Isolate* isolate, Zone* zone)
    : objects_to_relocate(isolate->heap(), ZoneAllocationPolicy(zone)) {}

CodeSpecialization::~CodeSpecialization() {}

void CodeSpecialization::RelocateMemoryReferences(Address old_start,
                                                  uint32_t old_size,
                                                  Address new_start,
                                                  uint32_t new_size) {
  DCHECK(old_mem_start == nullptr && old_mem_size == 0 &&
         new_mem_start == nullptr && new_mem_size == 0);
  DCHECK(old_start != new_start || old_size != new_size);
  old_mem_start = old_start;
  old_mem_size = old_size;
  new_mem_start = new_start;
  new_mem_size = new_size;
}

void CodeSpecialization::RelocateGlobals(Address old_start, Address new_start) {
  DCHECK(old_globals_start == 0 && new_globals_start == 0);
  DCHECK(old_start != 0 || new_start != 0);
  old_globals_start = old_start;
  new_globals_start = new_start;
}

void CodeSpecialization::PatchTableSize(uint32_t old_size, uint32_t new_size) {
  DCHECK(old_function_table_size == 0 && new_function_table_size == 0);
  DCHECK(old_size != 0 || new_size != 0);
  old_function_table_size = old_size;
  new_function_table_size = new_size;
}

void CodeSpecialization::RelocateDirectCalls(
    Handle<WasmInstanceObject> instance) {
  DCHECK(relocate_direct_calls_instance.is_null());
  DCHECK(!instance.is_null());
  relocate_direct_calls_instance = instance;
}

void CodeSpecialization::RelocateObject(Handle<Object> old_obj,
                                        Handle<Object> new_obj) {
  DCHECK(!old_obj.is_null() && !new_obj.is_null());
  has_objects_to_relocate = true;
  objects_to_relocate.Set(*old_obj, new_obj);
}

bool CodeSpecialization::ApplyToWholeInstance(
    WasmInstanceObject* instance, ICacheFlushMode icache_flush_mode) {
  DisallowHeapAllocation no_gc;
  WasmCompiledModule* compiled_module = instance->compiled_module();
  FixedArray* code_table = compiled_module->ptr_to_code_table();
  WasmModule* module = compiled_module->module();
  std::vector<WasmFunction>* wasm_functions =
      &compiled_module->module()->functions;
  DCHECK_EQ(wasm_functions->size() +
                compiled_module->module()->num_exported_functions,
            code_table->length());

  bool changed = false;
  int func_index = module->num_imported_functions;

  // Patch all wasm functions.
  for (int num_wasm_functions = static_cast<int>(wasm_functions->size());
       func_index < num_wasm_functions; ++func_index) {
    Code* wasm_function = Code::cast(code_table->get(func_index));
    if (wasm_function->kind() != Code::WASM_FUNCTION) continue;
    changed |= ApplyToWasmCode(wasm_function, icache_flush_mode);
  }

  // Patch all exported functions (if we shall relocate direct calls).
  if (!relocate_direct_calls_instance.is_null()) {
    // If we patch direct calls, the instance registered for that
    // (relocate_direct_calls_instance) should match the instance we currently
    // patch (instance).
    DCHECK_EQ(instance, *relocate_direct_calls_instance);
    for (auto exp : module->export_table) {
      if (exp.kind != kExternalFunction) continue;
      Code* export_wrapper = Code::cast(code_table->get(func_index));
      DCHECK_EQ(Code::JS_TO_WASM_FUNCTION, export_wrapper->kind());
      // There must be exactly one call to WASM_FUNCTION or WASM_TO_JS_FUNCTION.
      for (RelocIterator it(export_wrapper,
                            RelocInfo::ModeMask(RelocInfo::CODE_TARGET));
           ; it.next()) {
        DCHECK(!it.done());
        // Ignore calls to other builtins like ToNumber.
        if (!IsAtWasmDirectCallTarget(it)) continue;
        Code* new_code = Code::cast(code_table->get(exp.index));
        it.rinfo()->set_target_address(new_code->GetIsolate(),
                                       new_code->instruction_start(),
                                       UPDATE_WRITE_BARRIER, SKIP_ICACHE_FLUSH);
        break;
      }
      changed = true;
      func_index++;
    }
    DCHECK_EQ(code_table->length(), func_index);
  }
  return changed;
}

bool CodeSpecialization::ApplyToWasmCode(Code* code,
                                         ICacheFlushMode icache_flush_mode) {
  DisallowHeapAllocation no_gc;
  DCHECK_EQ(Code::WASM_FUNCTION, code->kind());

  bool reloc_mem_addr = old_mem_start != new_mem_start;
  bool reloc_mem_size = old_mem_size != new_mem_size;
  bool reloc_globals = old_globals_start || new_globals_start;
  bool patch_table_size = old_function_table_size || new_function_table_size;
  bool reloc_direct_calls = !relocate_direct_calls_instance.is_null();
  bool reloc_objects = has_objects_to_relocate;

  int reloc_mode = 0;
  auto add_mode = [&reloc_mode](bool cond, RelocInfo::Mode mode) {
    if (cond) reloc_mode |= RelocInfo::ModeMask(mode);
  };
  add_mode(reloc_mem_addr, RelocInfo::WASM_MEMORY_REFERENCE);
  add_mode(reloc_mem_size, RelocInfo::WASM_MEMORY_SIZE_REFERENCE);
  add_mode(reloc_globals, RelocInfo::WASM_GLOBAL_REFERENCE);
  add_mode(patch_table_size, RelocInfo::WASM_FUNCTION_TABLE_SIZE_REFERENCE);
  add_mode(reloc_direct_calls, RelocInfo::CODE_TARGET);
  add_mode(reloc_objects, RelocInfo::EMBEDDED_OBJECT);

  std::unique_ptr<PatchDirectCallsHelper> patch_direct_calls_helper;
  bool changed = false;

  for (RelocIterator it(code, reloc_mode); !it.done(); it.next()) {
    RelocInfo::Mode mode = it.rinfo()->rmode();
    switch (mode) {
      case RelocInfo::WASM_MEMORY_REFERENCE:
        DCHECK(reloc_mem_addr);
        it.rinfo()->update_wasm_memory_reference(code->GetIsolate(),
                                                 old_mem_start, new_mem_start,
                                                 icache_flush_mode);
        changed = true;
        break;
      case RelocInfo::WASM_MEMORY_SIZE_REFERENCE:
        DCHECK(reloc_mem_size);
        it.rinfo()->update_wasm_memory_size(code->GetIsolate(), old_mem_size,
                                            new_mem_size, icache_flush_mode);
        changed = true;
        break;
      case RelocInfo::WASM_GLOBAL_REFERENCE:
        DCHECK(reloc_globals);
        it.rinfo()->update_wasm_global_reference(
            code->GetIsolate(), old_globals_start, new_globals_start,
            icache_flush_mode);
        changed = true;
        break;
      case RelocInfo::CODE_TARGET: {
        DCHECK(reloc_direct_calls);
        // Skip everything which is not a wasm call (stack checks, traps, ...).
        if (!IsAtWasmDirectCallTarget(it)) continue;
        // Iterate simultaneously over the relocation information and the source
        // position table. For each call in the reloc info, move the source
        // position iterator forward to that position to find the byte offset of
        // the respective call. Then extract the call index from the module wire
        // bytes to find the new compiled function.
        size_t offset = it.rinfo()->pc() - code->instruction_start();
        if (!patch_direct_calls_helper) {
          patch_direct_calls_helper.reset(new PatchDirectCallsHelper(
              *relocate_direct_calls_instance, code));
        }
        int byte_pos = AdvanceSourcePositionTableIterator(
            patch_direct_calls_helper->source_pos_it, offset);
        int called_func_index = ExtractDirectCallIndex(
            patch_direct_calls_helper->decoder,
            patch_direct_calls_helper->func_bytes + byte_pos);
        FixedArray* code_table =
            relocate_direct_calls_instance->compiled_module()
                ->ptr_to_code_table();
        Code* new_code = Code::cast(code_table->get(called_func_index));
        it.rinfo()->set_target_address(new_code->GetIsolate(),
                                       new_code->instruction_start(),
                                       UPDATE_WRITE_BARRIER, icache_flush_mode);
        changed = true;
      } break;
      case RelocInfo::EMBEDDED_OBJECT: {
        DCHECK(reloc_objects);
        Object* old = it.rinfo()->target_object();
        Handle<Object>* new_obj = objects_to_relocate.Find(old);
        if (new_obj) {
          it.rinfo()->set_target_object(HeapObject::cast(**new_obj),
                                        UPDATE_WRITE_BARRIER,
                                        icache_flush_mode);
          changed = true;
        }
      } break;
      case RelocInfo::WASM_FUNCTION_TABLE_SIZE_REFERENCE:
        DCHECK(patch_table_size);
        it.rinfo()->update_wasm_function_table_size_reference(
            code->GetIsolate(), old_function_table_size,
            new_function_table_size, icache_flush_mode);
        changed = true;
        break;
      default:
        UNREACHABLE();
    }
  }

  return changed;
}
