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

#include "platform/atomic.h"
#include "platform/thread_sanitizer.h"
#include "vm/code_descriptors.h"
#include "vm/object.h"

namespace dart {

#if defined(USING_THREAD_SANITIZER) && !defined(DART_PRECOMPILED_RUNTIME)

struct TsanLineNumberProgram {
  TsanLineNumberProgram* next;
  uintptr_t pc;
  uint32_t size;
  uint8_t stream[];
};
static std::atomic<TsanLineNumberProgram*> head = nullptr;

enum : uint8_t {
  OP_STOP = 0,
  OP_PUSH_FUNCTION,
  OP_POP_FUNCTION,
  OP_ADVANCE_PC,
  OP_CHANGE_POSITION,
};

static void WriteString(GrowableArray<uint8_t>& out, const char* str) {
  for (intptr_t i = 0, len = strlen(str); i <= len; i++) {
    out.Add(str[i]);
  }
}

void RegisterTsanSymbolize(const Code& code) {
  GrowableArray<uint8_t> out(256);

  if (code.IsFunctionCode()) {
    const CodeSourceMap& map = CodeSourceMap::Handle(code.code_source_map());
    RELEASE_ASSERT(!map.IsNull());
    const Array& functions = Array::Handle(code.inlined_id_to_function());
    Function& function = Function::Handle(code.function());
    Script& script = Script::Handle(function.script());
    String& url = String::Handle(script.url());
    GrowableArray<Function*> function_stack(8);
    GrowableArray<TokenPosition> token_positions(8);
    function_stack.Add(&Function::Handle(function.ptr()));
    token_positions.Add(CodeSourceMapBuilder::kInitialPosition);

    out.Add(OP_PUSH_FUNCTION);
    WriteString(out, function.QualifiedUserVisibleNameCString());
    WriteString(out, url.ToCString());

    NoSafepointScope no_safepoint;
    ReadStream stream(map.Data(), map.Length());
    while (stream.PendingBytes() > 0) {
      int32_t arg;
      switch (CodeSourceMapOps::Read(&stream, &arg)) {
        case CodeSourceMapOps::kChangePosition: {
          const TokenPosition& old_token = token_positions.Last();
          token_positions.Last() = TokenPosition::Deserialize(
              Utils::AddWithWrapAround(arg, old_token.Serialize()));

          TokenPosition pos = token_positions.Last();
          if (pos.IsNoSource()) {
            pos = function_stack.Last()->token_pos();
          }

          intptr_t line = -1;
          intptr_t column = -1;
          script = function_stack.Last()->script();
          script.GetTokenLocation(pos, &line, &column);

          out.Add(OP_CHANGE_POSITION);
          out.Add((line >> 0) & 0xFF);
          out.Add((line >> 8) & 0xFF);
          out.Add((column >> 0) & 0xFF);
          out.Add((column >> 8) & 0xFF);
          break;
        }
        case CodeSourceMapOps::kAdvancePC: {
          out.Add(OP_ADVANCE_PC);
          out.Add((arg >> 0) & 0xFF);
          out.Add((arg >> 8) & 0xFF);
          out.Add((arg >> 16) & 0xFF);
          out.Add((arg >> 24) & 0xFF);
          break;
        }
        case CodeSourceMapOps::kPushFunction: {
          function ^= functions.At(arg);
          function_stack.Add(&Function::Handle(function.ptr()));
          token_positions.Add(CodeSourceMapBuilder::kInitialPosition);

          out.Add(OP_PUSH_FUNCTION);
          WriteString(out, function.QualifiedUserVisibleNameCString());
          script = function_stack.Last()->script();
          url = script.url();
          WriteString(out, url.ToCString());
          break;
        }
        case CodeSourceMapOps::kPopFunction: {
          function_stack.RemoveLast();
          token_positions.RemoveLast();
          out.Add(OP_POP_FUNCTION);
          break;
        }
        case CodeSourceMapOps::kNullCheck: {
          break;
        }
        default:
          UNREACHABLE();
      }
    }
  } else {
    out.Add(OP_PUSH_FUNCTION);
    WriteString(out, code.Name());
    WriteString(out, "stub");

    int32_t arg = code.Size();
    out.Add(OP_ADVANCE_PC);
    out.Add((arg >> 0) & 0xFF);
    out.Add((arg >> 8) & 0xFF);
    out.Add((arg >> 16) & 0xFF);
    out.Add((arg >> 24) & 0xFF);
  }
  out.Add(OP_STOP);

  TsanLineNumberProgram* lnp = reinterpret_cast<TsanLineNumberProgram*>(
      malloc(sizeof(TsanLineNumberProgram) + out.length()));
  lnp->next = nullptr;
  lnp->pc = code.PayloadStart();
  lnp->size = code.Size();
  memcpy(&lnp->stream, out.data(), out.length());  // NOLINT

  TsanLineNumberProgram* old_head = head.load(std::memory_order_acquire);
  do {
    lnp->next = old_head;
  } while (
      !head.compare_exchange_weak(old_head, lnp, std::memory_order_acq_rel));
}

typedef void (*AddFrame)(void* ctxt,
                         const char* function_name,
                         const char* file,
                         int line,
                         int column);

// It would be nice to implement this by a heap walk to find the Code object and
// then symbolize using our normal PC descriptors, etc, but this function must
// not call any function that has been instrumented by TSAN or it might deadlock
// during __tsan_func_entry.
extern "C" __attribute__((visibility("default")))
__attribute((used)) NO_SANITIZE_THREAD DISABLE_SANITIZER_INSTRUMENTATION void
__tsan_symbolize_external_ex(uintptr_t pc, AddFrame add_frame, void* ctxt) {
  const uword lookup_pc = pc & ~kExternalPCBit;

  for (TsanLineNumberProgram* lnp = head.load(std::memory_order_acquire);
       lnp != nullptr; lnp = lnp->next) {
    if ((lookup_pc >= lnp->pc) && ((lookup_pc - lnp->pc) < lnp->size)) {
      // Greater than the default value of --inlining_depth_threshold.
      constexpr intptr_t kMaxDepth = 32;
      const char* names[kMaxDepth];
      const char* files[kMaxDepth];
      int32_t lines[kMaxDepth];
      int32_t columns[kMaxDepth];
      intptr_t depth = 0;
      uword lnp_pc = lnp->pc;

      uint8_t* cursor = &lnp->stream[0];
      for (;;) {
        switch (*cursor++) {
          case OP_PUSH_FUNCTION: {
            const char* name = reinterpret_cast<const char*>(cursor);
            while (*cursor++ != 0) {
            }
            const char* file = reinterpret_cast<const char*>(cursor);
            while (*cursor++ != 0) {
            }

            names[depth] = name;
            files[depth] = file;
            lines[depth] = -1;
            columns[depth] = -1;

            depth++;
            if (depth >= kMaxDepth) {
              FATAL("LNP overflow");
            }
            break;
          }
          case OP_POP_FUNCTION: {
            depth--;
            break;
          }
          case OP_CHANGE_POSITION: {
            uint8_t a = *cursor++;
            uint8_t b = *cursor++;
            uint8_t c = *cursor++;
            uint8_t d = *cursor++;
            lines[depth - 1] = a | (b << 8);
            columns[depth - 1] = c | (d << 8);
            break;
          }
          case OP_ADVANCE_PC: {
            uint8_t a = *cursor++;
            uint8_t b = *cursor++;
            uint8_t c = *cursor++;
            uint8_t d = *cursor++;
            uint32_t disp = a | (b << 8) | (c << 16) | (d << 24);
            lnp_pc += disp;
            if (lookup_pc <= lnp_pc) {
              for (intptr_t i = depth - 1; i >= 0; i--) {
                add_frame(ctxt, names[i], files[i], lines[i], columns[i]);
              }
              return;
            }
            break;
          }
          case OP_STOP: {
            FATAL("pc in function but outside LNP");
          }
          default:
            UNREACHABLE();
        }
      }
      UNREACHABLE();
    }
  }

  // TSAN doesn't fall back to reporting the PC if we provide no frame or a
  // frame with a null location.
  char pc_string[17];
  for (intptr_t i = 0; i < 16; i++) {
    pc_string[i] = "0123456789abcdef"[(lookup_pc >> ((15 - i) << 2)) & 0xF];
  }
  pc_string[16] = 0;

  if (lookup_pc == 42) {
    add_frame(ctxt, "dart-deoptimized-code", pc_string, 0, 0);
  } else {
    add_frame(ctxt, "dart-code-lookup-failed", pc_string, 0, 0);
  }
}
#else
void RegisterTsanSymbolize(const Code& code) {}
#endif

}  // namespace dart
