@ 261106.4
current
// Copyright 2026 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/debug/debug-scope-info.h"

#include <cstddef>
#include <iterator>
#include <type_traits>

#include "src/ast/ast-value-factory.h"
#include "src/ast/scopes.h"
#include "src/base/bit-field.h"
#include "src/base/numerics/safe_conversions.h"
#include "src/base/vector.h"
#include "src/common/globals.h"
#include "src/debug/debug.h"
#include "src/execution/isolate-inl.h"
#include "src/handles/handles-inl.h"
#include "src/heap/factory.h"
#include "src/objects/debug-objects-inl.h"
#include "src/objects/fixed-array-inl.h"
#include "src/objects/string-inl.h"
#include "src/parsing/parse-info.h"
#include "src/parsing/parsing.h"
#include "src/zone/zone-containers.h"

namespace v8 {
namespace internal {

// ===========================================================================
// ByteArray: numeric_data
// ===========================================================================
// +-------------------------------------------------------------------------+
// | Header (4 bytes):                                                       |
// |   int32_t scope_count                                                   |
// +-------------------------------------------------------------------------+
// | Offset Table (scope_count * 4 bytes):                                   |
// |   uint32_t scope_offsets[scope_count]                                   |
// +-------------------------------------------------------------------------+
// | Scope Record 0 (at scope_offsets[0]):                                   |
// |   +0: int32_t  start_position                                           |
// |   +4: int32_t  end_position                                             |
// |   +8: int32_t  parent_scope_index (-1 for root script scope)            |
// |  +12: uint32_t flags (ScopeType, IsHidden, NeedsContext, HasSibling, etc)|
// |  +16: int32_t  var_count                                                |
// |  --- Dynamic Optional Fields (present conditionally based on flags) --- |
// |  [+20: int32_t  next_sibling_index]        (if HasSiblingBit is set)    |
// |  [+..: int32_t  context_id]                (if NeedsContextBit is set)  |
// |  [+..: int32_t  receiver_allocation_info]  (if HasThisDeclBit is set)   |
// |  [+..: int32_t  arguments_allocation_info] (if HasArgumentsBit is set)  |
// |  [+..: int32_t + int32_t function_var]     (if HasFunctionVarBit is set)|
// |  --- Variables Array (var_count entries, 14 bytes each, packed) ---     |
// |  [DebugVariableEntry 0]:                                                |
// |     +0: int32_t  slot_index                                             |
// |     +4: uint16_t location_mode_flags                                    |
// |     +6: int32_t  initializer_position                                   |
// |    +10: int32_t  name_index (into string_table FixedArray)              |
// |  [DebugVariableEntry 1]...                                              |
// +-------------------------------------------------------------------------+
// | Scope Record 1 (at scope_offsets[1])...                                 |
// +-------------------------------------------------------------------------+

namespace {

struct ScopeRecord {
  int32_t start_position;
  int32_t end_position;
  int32_t parent_scope_index;
  uint32_t flags;
  // The number of declarations in a single scope is not bounded by anything
  // the parser enforces, so this routinely exceeds 2^16 for generated code.
  int32_t var_count;
};

static_assert(std::is_trivial_v<ScopeRecord>);
static_assert(std::is_standard_layout_v<ScopeRecord>);
// Ensure ScopeRecord has no padding. Update when adding new fields.
static_assert(sizeof(ScopeRecord) == 20);

// Packed so that the entry stays 14 bytes instead of being padded to 16. The
// struct is only ever read and written as a whole via
// base::ReadUnalignedValue/base::WriteUnalignedValue, so the reduced alignment
// is unobservable and no member address is ever taken.
struct __attribute__((packed)) DebugVariableEntry {
  // Variable::index(): a context slot, a stack slot or a parameter index.
  // Context and stack slots grow with the scope's variable count and do
  // exceed 2^16. Negative values are meaningful: -1 for unallocated variables
  // and for the receiver, and module imports use negative indices.
  int32_t slot_index;
  uint16_t location_mode_flags;
  int32_t initializer_position;
  int32_t name_index;
};

static_assert(std::is_trivial_v<DebugVariableEntry>);
static_assert(std::is_standard_layout_v<DebugVariableEntry>);
static_assert(sizeof(DebugVariableEntry) == 14);

// Note: These bits are stored in ScopeRecord::flags, which is 32 bits wide
// because the 16 available bits were exhausted. Once ScopeIterator is fully
// migrated onto DebugScriptScopeInfo some of these may turn out to be
// redundant; revisit whether the word can shrink back to 16 bits then.
using ScopeTypeBits = base::BitField<ScopeType, 0, 4, uint32_t>;
using HasChildrenBit = ScopeTypeBits::Next<bool, 1>;
using HasSiblingBit = HasChildrenBit::Next<bool, 1>;
using IsHiddenBit = HasSiblingBit::Next<bool, 1>;
using LanguageModeBit = IsHiddenBit::Next<LanguageMode, 1>;
using IsDeclarationScopeBit = LanguageModeBit::Next<bool, 1>;
using IsArrowScopeBit = IsDeclarationScopeBit::Next<bool, 1>;
using HasThisDeclarationBit = IsArrowScopeBit::Next<bool, 1>;
using HasThisReferenceBit = HasThisDeclarationBit::Next<bool, 1>;
using HasSimpleParametersBit = HasThisReferenceBit::Next<bool, 1>;
using SloppyEvalCanExtendVarsBit = HasSimpleParametersBit::Next<bool, 1>;
using NeedsContextBit = SloppyEvalCanExtendVarsBit::Next<bool, 1>;
using HasArgumentsBit = NeedsContextBit::Next<bool, 1>;
using HasFunctionVarBit = HasArgumentsBit::Next<bool, 1>;
static_assert(HasFunctionVarBit::kLastUsedBit < 32);

using VariableLocationBits = base::BitField<VariableLocation, 0, 4, uint16_t>;
using VariableModeBits = VariableLocationBits::Next<VariableMode, 4>;
using IsSyntheticBit = VariableModeBits::Next<bool, 1>;
using IsReceiverBit = IsSyntheticBit::Next<bool, 1>;
static_assert(IsReceiverBit::kLastUsedBit < 16);

// Encodes receiver, arguments, or function variable allocation info into a
// 32-bit word:
// - Bits 0..1: VariableAllocationInfo (NONE, STACK, CONTEXT, UNUSED)
// - Bits 2..31: 30-bit signed slot/parameter index
//
// In V8, stack-allocated receiver variables have parameter index -1 (allocated
// via DeclarationScope::AllocateReceiver). Therefore, the index field is
// treated as a signed two's-complement integer, requiring sign-extension upon
// decoding.
//
// A 16-bit word is not enough: context slot indices are only bounded by the
// number of variables in the scope, and DeclarationScope::AllocateLocals()
// deliberately allocates the function variable last, giving it the highest
// index in its scope.
constexpr int kAllocInfoIndexShift = 2;
constexpr int kMaxAllocInfoIndex = (1 << 29) - 1;

int32_t EncodeAllocInfo(VariableAllocationInfo info, int index) {
  DCHECK_GE(index, -1);
  // Deliberately a CHECK: silently truncating here would hand the debugger a
  // plausible-looking but wrong slot index.
  CHECK_LE(index, kMaxAllocInfoIndex);
  return static_cast<int32_t>(
      (static_cast<uint32_t>(index) << kAllocInfoIndexShift) |
      static_cast<uint32_t>(info));
}

std::pair<VariableAllocationInfo, int> DecodeAllocInfo(int32_t val) {
  // C++20 guarantees an arithmetic (sign-extending) shift for signed types.
  return {static_cast<VariableAllocationInfo>(val & 3),
          val >> kAllocInfoIndexShift};
}

int32_t GetScopeCount(Tagged<DebugScriptScopeInfo> info) {
  Tagged<ByteArray> bytes = info->numeric_data();
  DCHECK_GE(bytes->length().value(), kInt32Size);
  return base::ReadUnalignedValue<int32_t>(bytes->begin());
}

uint32_t GetScopeOffset(Tagged<DebugScriptScopeInfo> info, int scope_index) {
  CHECK_GE(scope_index, 0);
  CHECK_LT(scope_index, GetScopeCount(info));
  const uint8_t* ptr =
      info->numeric_data()->begin() + kInt32Size + scope_index * kUInt32Size;
  return base::ReadUnalignedValue<uint32_t>(ptr);
}

class ByteArrayWriter {
 public:
  explicit ByteArrayWriter(Address cursor) : cursor_(cursor) {}

  template <typename T>
  void Write(const T& value) {
    static_assert(std::is_trivially_copyable_v<T>);
    base::WriteUnalignedValue<T>(cursor_, value);
    cursor_ += sizeof(T);
  }

  Address cursor() const { return cursor_; }

 private:
  Address cursor_;
};

}  // namespace

const uint8_t* DebugScriptScope::payload() const {
  return info_->numeric_data()->begin() + offset_;
}

uint32_t DebugScriptScope::flags() const {
  return base::ReadUnalignedValue<ScopeRecord>(payload()).flags;
}

DebugScriptScope DebugScriptScope::FromIndex(
    DirectHandle<DebugScriptScopeInfo> info, int scope_index) {
  CHECK_GE(scope_index, 0);
  CHECK_LT(scope_index, GetScopeCount(*info));
  uint32_t offset = GetScopeOffset(*info, scope_index);
  return DebugScriptScope(info, scope_index, offset);
}

std::optional<DebugScriptScope> DebugScriptScope::parent() const {
  int parent_idx = parent_index();
  if (parent_idx == -1) return std::nullopt;
  return FromIndex(info_, parent_idx);
}

std::optional<DebugScriptScope> DebugScriptScope::first_child() const {
  if (!HasChildrenBit::decode(flags())) return std::nullopt;
  return FromIndex(info_, scope_index_ + 1);
}

std::optional<DebugScriptScope> DebugScriptScope::next_sibling() const {
  if (!HasSiblingBit::decode(flags())) return std::nullopt;
  const uint8_t* sibling_ptr = payload() + next_sibling_offset();
  int sibling_idx = base::ReadUnalignedValue<int32_t>(sibling_ptr);
  return FromIndex(info_, sibling_idx);
}

int DebugScriptScope::start_position() const {
  return base::ReadUnalignedValue<ScopeRecord>(payload()).start_position;
}

int DebugScriptScope::end_position() const {
  return base::ReadUnalignedValue<ScopeRecord>(payload()).end_position;
}

bool DebugScriptScope::ContainsPosition(int position,
                                        bool is_closure_found) const {
  // In case the closure scope hasn't been found yet, we are less strict about
  // recursing downwards. This might be the case for nested arrow functions
  // that have the same end position.
  const bool position_fits_end =
      is_closure_found ? position < end_position() : position <= end_position();
  // While we're evaluating a class, the calling function will have a class
  // context on the stack with a range that starts at Token::kClass, and the
  // source position will also point to Token::kClass. To identify the matching
  // scope we include start in the accepted range for class scopes.
  //
  // Similarly "with" scopes can already have bytecodes where the source
  // position points to the closing parenthesis with the "with" context
  // already pushed.
  const bool position_fits_start = is_class_scope() || is_with_scope()
                                       ? start_position() <= position
                                       : start_position() < position;
  return position_fits_start && position_fits_end;
}

int DebugScriptScope::parent_index() const {
  return base::ReadUnalignedValue<ScopeRecord>(payload()).parent_scope_index;
}

ScopeType DebugScriptScope::scope_type() const {
  return ScopeTypeBits::decode(flags());
}

bool DebugScriptScope::is_script_scope() const {
  return scope_type() == ScopeType::SCRIPT_SCOPE ||
         scope_type() == ScopeType::REPL_MODE_SCOPE;
}

bool DebugScriptScope::is_function_scope() const {
  return scope_type() == ScopeType::FUNCTION_SCOPE;
}

bool DebugScriptScope::is_block_scope() const {
  return scope_type() == ScopeType::BLOCK_SCOPE ||
         scope_type() == ScopeType::CLASS_SCOPE;
}

bool DebugScriptScope::is_declaration_scope() const {
  return IsDeclarationScopeBit::decode(flags());
}

LanguageMode DebugScriptScope::language_mode() const {
  return LanguageModeBit::decode(flags());
}

bool DebugScriptScope::is_arrow_scope() const {
  return IsArrowScopeBit::decode(flags());
}

bool DebugScriptScope::is_class_scope() const {
  return scope_type() == ScopeType::CLASS_SCOPE;
}

bool DebugScriptScope::is_with_scope() const {
  return scope_type() == ScopeType::WITH_SCOPE;
}

bool DebugScriptScope::is_module_scope() const {
  return scope_type() == ScopeType::MODULE_SCOPE;
}

bool DebugScriptScope::is_eval_scope() const {
  return scope_type() == ScopeType::EVAL_SCOPE;
}

bool DebugScriptScope::is_catch_scope() const {
  return scope_type() == ScopeType::CATCH_SCOPE;
}

bool DebugScriptScope::is_repl_mode_scope() const {
  return scope_type() == ScopeType::REPL_MODE_SCOPE;
}

bool DebugScriptScope::is_hidden() const {
  return IsHiddenBit::decode(flags());
}

bool DebugScriptScope::has_this_declaration() const {
  return HasThisDeclarationBit::decode(flags());
}

bool DebugScriptScope::has_this_reference() const {
  return HasThisReferenceBit::decode(flags());
}

bool DebugScriptScope::has_simple_parameters() const {
  return HasSimpleParametersBit::decode(flags());
}

bool DebugScriptScope::has_arguments() const {
  return HasArgumentsBit::decode(flags());
}

bool DebugScriptScope::has_function_variable() const {
  return HasFunctionVarBit::decode(flags());
}

bool DebugScriptScope::sloppy_eval_can_extend_vars() const {
  return SloppyEvalCanExtendVarsBit::decode(flags());
}

bool DebugScriptScope::needs_context() const {
  return NeedsContextBit::decode(flags());
}

size_t DebugScriptScope::next_sibling_offset() const {
  return sizeof(ScopeRecord);
}

size_t DebugScriptScope::context_id_offset() const {
  return next_sibling_offset() +
         (HasSiblingBit::decode(flags()) ? kInt32Size : 0);
}

size_t DebugScriptScope::receiver_info_offset() const {
  return context_id_offset() +
         (NeedsContextBit::decode(flags()) ? kInt32Size : 0);
}

size_t DebugScriptScope::arguments_info_offset() const {
  return receiver_info_offset() +
         (HasThisDeclarationBit::decode(flags()) ? kInt32Size : 0);
}

size_t DebugScriptScope::function_variable_offset() const {
  return arguments_info_offset() +
         (HasArgumentsBit::decode(flags()) ? kInt32Size : 0);
}

size_t DebugScriptScope::variables_offset() const {
  return function_variable_offset() +
         (HasFunctionVarBit::decode(flags()) ? (2 * kInt32Size) : 0);
}

size_t DebugScriptScope::record_size() const {
  return variables_offset() + variable_count() * sizeof(DebugVariableEntry);
}

int DebugScriptScope::unique_id_in_script() const {
  if (!needs_context()) return -3;
  return base::ReadUnalignedValue<int32_t>(payload() + context_id_offset());
}

std::pair<VariableAllocationInfo, int> DebugScriptScope::receiver_info() const {
  if (!has_this_declaration()) return {VariableAllocationInfo::NONE, -1};
  return DecodeAllocInfo(
      base::ReadUnalignedValue<int32_t>(payload() + receiver_info_offset()));
}

std::pair<VariableAllocationInfo, int> DebugScriptScope::arguments_info()
    const {
  if (!has_arguments()) return {VariableAllocationInfo::NONE, -1};
  return DecodeAllocInfo(
      base::ReadUnalignedValue<int32_t>(payload() + arguments_info_offset()));
}

const uint8_t* DebugScriptScope::function_variable_payload() const {
  if (!has_function_variable()) return nullptr;
  return payload() + function_variable_offset();
}

std::pair<VariableAllocationInfo, int>
DebugScriptScope::function_variable_info() const {
  const uint8_t* ptr = function_variable_payload();
  if (!ptr) return {VariableAllocationInfo::NONE, -1};
  return DecodeAllocInfo(base::ReadUnalignedValue<int32_t>(ptr));
}

Tagged<InternalizedString> DebugScriptScope::function_variable_name() const {
  const uint8_t* ptr = function_variable_payload();
  if (!ptr) return {};
  int32_t name_index = base::ReadUnalignedValue<int32_t>(ptr + kInt32Size);
  CHECK_GE(name_index, 0);
  CHECK_LT(static_cast<uint32_t>(name_index),
           info_->string_table()->length().value());
  return CheckedCast<InternalizedString>(
      info_->string_table()->get(name_index));
}

int DebugScriptScope::variable_count() const {
  return base::ReadUnalignedValue<ScopeRecord>(payload()).var_count;
}

const uint8_t* DebugScriptScope::variables_payload() const {
  return payload() + variables_offset();
}

DebugVariableInfo DebugScriptScope::variable(int index) const {
  CHECK_GE(index, 0);
  CHECK_LT(index, variable_count());
  const uint8_t* entry_ptr =
      variables_payload() + index * sizeof(DebugVariableEntry);
  DebugVariableEntry entry =
      base::ReadUnalignedValue<DebugVariableEntry>(entry_ptr);
  CHECK_GE(entry.name_index, 0);
  CHECK_LT(static_cast<uint32_t>(entry.name_index),
           info_->string_table()->length().value());
  Tagged<InternalizedString> name = CheckedCast<InternalizedString>(
      info_->string_table()->get(entry.name_index));
  return DebugVariableInfo{
      .name = name,
      .location = VariableLocationBits::decode(entry.location_mode_flags),
      .index = entry.slot_index,
      .mode = VariableModeBits::decode(entry.location_mode_flags),
      .initializer_position = entry.initializer_position,
      .is_synthetic = IsSyntheticBit::decode(entry.location_mode_flags),
      .is_receiver = IsReceiverBit::decode(entry.location_mode_flags),
  };
}

Handle<DebugScriptScopeInfo> SerializeDebugScriptScopeInfo(
    Isolate* isolate, DeclarationScope* script_scope) {
  DCHECK_NOT_NULL(script_scope);
  DCHECK(script_scope->is_toplevel_scope());

  Zone* zone = script_scope->zone();
  ZoneVector<const AstRawString*> string_table(zone);
  ZoneAbslFlatHashMap<const AstRawString*, int32_t> string_map(zone);

  auto get_or_insert_string = [&](const AstRawString* raw_name) -> int32_t {
    CHECK_NOT_NULL(raw_name);
    auto it = string_map.find(raw_name);
    if (it != string_map.end()) return it->second;
    int32_t index = base::checked_cast<int32_t>(string_table.size());
    string_map.emplace(raw_name, index);
    string_table.push_back(raw_name);
    return index;
  };

  std::vector<Scope*> all_scopes;
  std::unordered_map<Scope*, int32_t> scope_to_index;

  auto collect = [&](auto& self, Scope* scope) -> void {
    int32_t index = base::checked_cast<int32_t>(all_scopes.size());
    all_scopes.push_back(scope);
    scope_to_index.emplace(scope, index);
    for (Scope* inner = scope->inner_scope(); inner != nullptr;
         inner = inner->sibling()) {
      self(self, inner);
    }
  };
  collect(collect, script_scope);
  DCHECK(!all_scopes.empty());
  DCHECK(!all_scopes[0]->is_with_scope());

  auto find_scope_index = [&](Scope* s) -> int32_t {
    if (s == nullptr) return -1;
    auto it = scope_to_index.find(s);
    return it != scope_to_index.end() ? it->second : -1;
  };
  DCHECK_EQ(find_scope_index(all_scopes[0]->outer_scope()), -1);

  // Stage 1: Pre-calculate exact required byte size and scope offsets.
  size_t total_size = kInt32Size + all_scopes.size() * kUInt32Size;
  std::vector<uint32_t> offsets;
  offsets.reserve(all_scopes.size());

  for (size_t i = 0; i < all_scopes.size(); ++i) {
    offsets.push_back(base::checked_cast<uint32_t>(total_size));
    total_size += sizeof(ScopeRecord);
    if (find_scope_index(all_scopes[i]->sibling()) != -1) {
      total_size += kInt32Size;
    }
    if (all_scopes[i]->NeedsContext()) {
      total_size += kInt32Size;
    }
    if (all_scopes[i]->is_declaration_scope() &&
        all_scopes[i]->AsDeclarationScope()->has_this_declaration()) {
      total_size += kInt32Size;
    }
    if (all_scopes[i]->is_declaration_scope() &&
        all_scopes[i]->AsDeclarationScope()->arguments() != nullptr) {
      total_size += kInt32Size;
    }
    if (all_scopes[i]->is_declaration_scope() &&
        all_scopes[i]->AsDeclarationScope()->function_var() != nullptr) {
      total_size += 2 * kInt32Size;
    }
    int32_t var_count = base::checked_cast<int32_t>(std::distance(
        all_scopes[i]->locals()->begin(), all_scopes[i]->locals()->end()));
    total_size += var_count * sizeof(DebugVariableEntry);
  }

  // Stage 2: Allocate a ByteArray and write directly into it with
  // ByteArrayWriter.
  Handle<ByteArray> byte_array = isolate->factory()->NewByteArray(
      base::checked_cast<uint32_t>(total_size), AllocationType::kOld);

  ByteArrayWriter header_writer(reinterpret_cast<Address>(byte_array->begin()));
  header_writer.Write<int32_t>(base::checked_cast<int32_t>(all_scopes.size()));

  for (size_t i = 0; i < all_scopes.size(); ++i) {
    header_writer.Write<uint32_t>(offsets[i]);
  }

  for (size_t i = 0; i < all_scopes.size(); ++i) {
    Address record =
        reinterpret_cast<Address>(byte_array->begin()) + offsets[i];
    Scope* scope = all_scopes[i];

    int32_t next_sibling = find_scope_index(scope->sibling());
    bool has_sibling = next_sibling != -1;
    bool needs_context = scope->NeedsContext();
    bool has_this_decl = false;
    bool has_arguments = false;
    bool has_function_var = false;

    uint32_t flags = 0;
    flags = ScopeTypeBits::update(flags, scope->scope_type());
    flags = HasChildrenBit::update(flags, scope->inner_scope() != nullptr);
    flags = HasSiblingBit::update(flags, has_sibling);
    flags = IsHiddenBit::update(flags, scope->is_hidden());
    flags = LanguageModeBit::update(flags, scope->language_mode());
    flags = IsDeclarationScopeBit::update(flags, scope->is_declaration_scope());
    flags = HasThisReferenceBit::update(flags, scope->HasThisReference());
    flags = NeedsContextBit::update(flags, needs_context);
    if (scope->is_declaration_scope()) {
      DeclarationScope* decl_scope = scope->AsDeclarationScope();
      has_this_decl = decl_scope->has_this_declaration();
      has_arguments = decl_scope->arguments() != nullptr;
      has_function_var = decl_scope->function_var() != nullptr;
      flags = IsArrowScopeBit::update(flags, decl_scope->is_arrow_scope());
      flags = HasThisDeclarationBit::update(flags, has_this_decl);
      flags = HasSimpleParametersBit::update(
          flags, decl_scope->has_simple_parameters());
      flags = SloppyEvalCanExtendVarsBit::update(
          flags, decl_scope->sloppy_eval_can_extend_vars());
    }
    flags = HasArgumentsBit::update(flags, has_arguments);
    flags = HasFunctionVarBit::update(flags, has_function_var);

    int32_t var_count = base::checked_cast<int32_t>(
        std::distance(scope->locals()->begin(), scope->locals()->end()));

    int32_t parent_scope_index = find_scope_index(scope->outer_scope());
    DCHECK_EQ(parent_scope_index == -1, i == 0);

    ByteArrayWriter writer(record);
    writer.Write<ScopeRecord>(ScopeRecord{
        .start_position = scope->start_position(),
        .end_position = scope->end_position(),
        .parent_scope_index = parent_scope_index,
        .flags = flags,
        .var_count = var_count,
    });

    if (has_sibling) {
      writer.Write<int32_t>(next_sibling);
    }
    if (needs_context) {
      // We only need the context ID to match DebugScopeInfo against runtime
      // ScopeInfo. V8 omits runtime ScopeInfo for any scope that doesn't need a
      // context so we don't need to waste the bytes.
      writer.Write<int32_t>(scope->UniqueIdInScript());
    }
    if (has_this_decl) {
      Variable* var = scope->AsDeclarationScope()->receiver();
      DCHECK_NE(var->location(), VariableLocation::LOOKUP);
      VariableAllocationInfo info = var->location() == VariableLocation::CONTEXT
                                        ? VariableAllocationInfo::CONTEXT
                                        : VariableAllocationInfo::STACK;
      writer.Write<int32_t>(EncodeAllocInfo(info, var->index()));
    }
    if (has_arguments) {
      Variable* var = scope->AsDeclarationScope()->arguments();
      DCHECK_NE(var->location(), VariableLocation::LOOKUP);
      VariableAllocationInfo info = var->location() == VariableLocation::CONTEXT
                                        ? VariableAllocationInfo::CONTEXT
                                        : VariableAllocationInfo::STACK;
      writer.Write<int32_t>(EncodeAllocInfo(info, var->index()));
    }
    if (has_function_var) {
      Variable* var = scope->AsDeclarationScope()->function_var();
      DCHECK_NE(var->location(), VariableLocation::LOOKUP);
      VariableAllocationInfo info = var->location() == VariableLocation::CONTEXT
                                        ? VariableAllocationInfo::CONTEXT
                                        : VariableAllocationInfo::STACK;
      writer.Write<int32_t>(EncodeAllocInfo(info, var->index()));
      int32_t name_index = get_or_insert_string(var->raw_name());
      writer.Write<int32_t>(name_index);
    }

    for (Variable* var : *scope->locals()) {
      DCHECK_EQ(var->scope(), scope);
      // Variables declared in locals() must be statically allocated (or
      // unallocated if unused). They must never require dynamic lookup against
      // an outer context (e.g. VariableLocation::LOOKUP), which would indicate
      // dependence on outer runtime scopes such as WithScopes.
      DCHECK_NE(var->location(), VariableLocation::LOOKUP);
      const AstRawString* raw = var->raw_name();
      // LINT.IfChange(VariableIsSynthetic)
      // Keep in sync with ScopeInfo::VariableIsSynthetic() in
      // src/objects/scope-info.cc.
      bool is_synthetic =
          raw != nullptr &&
          (raw->IsEmpty() || raw->FirstCharacter() == '.' ||
           raw->IsPrivateName() || raw->IsOneByteEqualTo("this"));
      // LINT.ThenChange(/src/objects/scope-info.cc:VariableIsSynthetic)
      // LINT.IfChange(VariableIsReceiver)
      // Keep in sync with Variable::IsReceiver() in src/ast/variables.h.
      // Variable::IsReceiver() asserts IsParameter() in debug builds.
      bool is_receiver = var->IsParameter() && var->IsReceiver();
      // LINT.ThenChange(/src/ast/variables.h:VariableIsReceiver)
      uint16_t var_flags = 0;
      var_flags = VariableLocationBits::update(var_flags, var->location());
      var_flags = VariableModeBits::update(var_flags, var->mode());
      var_flags = IsSyntheticBit::update(var_flags, is_synthetic);
      var_flags = IsReceiverBit::update(var_flags, is_receiver);

      writer.Write<DebugVariableEntry>(DebugVariableEntry{
          .slot_index = var->index(),
          .location_mode_flags = var_flags,
          .initializer_position = var->initializer_position(),
          .name_index = get_or_insert_string(var->raw_name()),
      });
    }

    size_t expected_size =
        (i + 1 < all_scopes.size() ? offsets[i + 1] : total_size) - offsets[i];
    CHECK_EQ(writer.cursor(), record + expected_size);
  }

  DirectHandle<FixedArray> final_string_table;
  if (string_table.empty()) {
    final_string_table = isolate->factory()->empty_fixed_array();
  } else {
    uint32_t string_count = base::checked_cast<uint32_t>(string_table.size());
    Handle<FixedArray> table =
        isolate->factory()->NewFixedArray(string_count, AllocationType::kOld);
    for (uint32_t i = 0; i < string_count; ++i) {
      DirectHandle<InternalizedString> str = string_table[i]->string();
      CHECK(IsInternalizedString(*str));
      table->set(i, *str);
    }
    final_string_table = table;
  }
  return isolate->factory()->NewDebugScriptScopeInfo(byte_array,
                                                     final_string_table);
}

Handle<DebugScriptScopeInfo> EnsureDebugScriptScopeInfo(
    Isolate* isolate, DirectHandle<Script> script) {
  DirectHandle<DebugScriptScopeInfo> cached_info =
      isolate->debug()->GetScriptScopeInfo(script);
  if (!cached_info.is_null()) {
    return handle(*cached_info, isolate);
  }

  CHECK(IsString(script->source()));

  // UnoptimizedCompileFlags::ForScriptCompile automatically initializes flags
  // from the script object:
  // - Sets is_toplevel(true) and preserves REPL and module flags.
  // - Restores outer_language_mode and compilation kind (host, eval, wrapped,
  //   Function constructor).
  // - For wrapped scripts, sets function_syntax_kind to kWrapped and is_eval.
  //
  // Eager compilation ensures all inner function scopes are parsed rather
  // than skipped. Marking as reparse prevents spawning background parallel
  // compile tasks and preserves context allocation flags.
  UnoptimizedCompileFlags flags =
      UnoptimizedCompileFlags::ForScriptCompile(isolate, *script)
          .set_is_eager(true);
  flags.set_is_reparse(true);

  MaybeDirectHandle<ScopeInfo> maybe_outer_scope;
  if (script->has_eval_from_scope_info()) {
    maybe_outer_scope =
        direct_handle(Cast<ScopeInfo>(script->eval_from_scope_info()), isolate);
  }

  UnoptimizedCompileState compile_state;
  ReusableUnoptimizedCompileState reusable_state(isolate);
  ParseInfo info(isolate, flags, &compile_state, &reusable_state);

  if (!parsing::ParseProgram(&info, script, maybe_outer_scope, isolate,
                             parsing::ReportStatisticsMode{false})) {
    return Handle<DebugScriptScopeInfo>::null();
  }

  Handle<DebugScriptScopeInfo> debug_info =
      SerializeDebugScriptScopeInfo(isolate, info.literal()->scope());
  isolate->debug()->SetScriptScopeInfo(script, debug_info);
  return debug_info;
}

int DebugScriptScopeCount(Tagged<DebugScriptScopeInfo> info) {
  return GetScopeCount(info);
}

std::optional<DebugScriptScope> FindClosureScope(
    DirectHandle<DebugScriptScopeInfo> info, int start_position,
    int end_position, ScopeType scope_type) {
  // SerializeDebugScriptScopeInfo() emits scopes in DFS pre-order, so scanning
  // the offset table linearly visits the scopes in the same order the AST
  // traversal did.
  const int scope_count = DebugScriptScopeCount(*info);
  for (int i = 0; i < scope_count; ++i) {
    DebugScriptScope scope = DebugScriptScope::FromIndex(info, i);
    if (scope.scope_type() == scope_type &&
        scope.start_position() == start_position &&
        scope.end_position() == end_position) {
      return scope;
    }
  }
  return std::nullopt;
}

namespace {

// Visits all descendants of `scope` and narrows `*best` down to the scope with
// the tightest bounds around `position`.
void NarrowToInnermostScope(DebugScriptScope scope, int position,
                            DebugScriptScope* best) {
  for (std::optional<DebugScriptScope> child = scope.first_child();
       child.has_value(); child = child->next_sibling()) {
    // Update `*best` if `child` contains `position` and is a tighter fit than
    // the currently best scope. Generators have the same source position as
    // the scope they belong to, so we also check for equality.
    if (child->ContainsPosition(position, /*is_closure_found=*/true) &&
        child->start_position() >= best->start_position() &&
        child->end_position() <= best->end_position()) {
      *best = *child;
    }
    NarrowToInnermostScope(*child, position, best);
  }
}

}  // namespace

DebugScriptScope FindInnermostScope(DebugScriptScope closure_scope,
                                    int position) {
  DebugScriptScope best = closure_scope;
  NarrowToInnermostScope(closure_scope, position, &best);
  return best;
}

#ifdef VERIFY_HEAP
// DebugScriptScopeInfo::DebugScriptScopeInfoVerify is placed here instead of in
// objects-debug.cc to keep the exact layout of numeric_data local to
// debug-scope-info.cc.
void DebugScriptScopeInfo::DebugScriptScopeInfoVerify(Isolate* isolate) {
  CHECK(Is<Struct>(this));
  CHECK(Is<DebugScriptScopeInfo>(this));
  Object::VerifyPointer(isolate, numeric_data_.load());
  Object::VerifyPointer(isolate, string_table_.load());
  CHECK(IsByteArray(numeric_data()));
  CHECK(IsFixedArray(string_table()));

  Tagged<ByteArray> bytes = numeric_data();
  CHECK_GE(bytes->length().value(), kInt32Size);
  int scope_count = GetScopeCount(this);
  CHECK_GE(scope_count, 0);

  size_t header_and_table_size = kInt32Size + scope_count * kUInt32Size;
  CHECK_GE(static_cast<size_t>(bytes->length().value()), header_and_table_size);

  HandleScope handle_scope(isolate);
  DirectHandle<DebugScriptScopeInfo> info_handle(this, isolate);

  for (int i = 0; i < scope_count; ++i) {
    uint32_t offset = GetScopeOffset(this, i);
    CHECK_EQ(offset % kUInt16Size, 0);
    CHECK_GE(offset, header_and_table_size);
    CHECK_LE(offset + sizeof(ScopeRecord),
             static_cast<size_t>(bytes->length().value()));
    DebugScriptScope scope = DebugScriptScope::FromIndex(info_handle, i);
    CHECK_EQ(scope.scope_index(), i);
    CHECK_LE(scope.start_position(), scope.end_position());

    size_t record_size = scope.record_size();
    CHECK_LE(offset + record_size,
             static_cast<size_t>(bytes->length().value()));

    // Top-level and function scopes are always declaration scopes. Block
    // scopes may or may not be (see IsDeclarationScopeBit).
    CHECK_IMPLIES(scope.is_script_scope() || scope.is_function_scope() ||
                      scope.is_module_scope() || scope.is_eval_scope(),
                  scope.is_declaration_scope());
    // The declaration-only flags must be clear for non-declaration scopes.
    CHECK_IMPLIES(!scope.is_declaration_scope(),
                  !scope.is_arrow_scope() && !scope.has_this_declaration() &&
                      !scope.has_simple_parameters() &&
                      !scope.has_arguments() &&
                      !scope.has_function_variable() &&
                      !scope.sloppy_eval_can_extend_vars());

    if (scope.needs_context()) {
      CHECK_GE(scope.unique_id_in_script(), -2);
    } else {
      CHECK_EQ(scope.unique_id_in_script(), -3);
    }

    if (!scope.has_this_declaration()) {
      CHECK_EQ(scope.receiver_info(),
               (std::pair{VariableAllocationInfo::NONE, -1}));
    }

    if (!scope.has_arguments()) {
      CHECK_EQ(scope.arguments_info(),
               (std::pair{VariableAllocationInfo::NONE, -1}));
    }

    if (!scope.has_function_variable()) {
      CHECK_EQ(scope.function_variable_info(),
               (std::pair{VariableAllocationInfo::NONE, -1}));
      CHECK(scope.function_variable_name().is_null());
    } else {
      CHECK(!scope.function_variable_name().is_null());
      const uint8_t* ptr = scope.function_variable_payload();
      CHECK_NOT_NULL(ptr);
      int32_t name_index = base::ReadUnalignedValue<int32_t>(ptr + kInt32Size);
      CHECK_GE(name_index, 0);
      CHECK_LT(static_cast<uint32_t>(name_index),
               string_table()->length().value());
      CHECK(IsInternalizedString(string_table()->get(name_index)));
    }

    CHECK_EQ(scope.variable_count(),
             base::ReadUnalignedValue<ScopeRecord>(scope.payload()).var_count);
    for (int v = 0; v < scope.variable_count(); ++v) {
      DebugVariableInfo var = scope.variable(v);
      CHECK(!var.name.is_null());
      CHECK(IsInternalizedString(var.name));
    }

    if (i == 0) {
      CHECK(!scope.parent().has_value());
    } else {
      CHECK(scope.parent().has_value());
      CHECK_GE(scope.parent()->scope_index(), 0);
      CHECK_LT(scope.parent()->scope_index(), i);
    }

    if (auto first_child = scope.first_child()) {
      CHECK_LT(i + 1, scope_count);
      CHECK_EQ(first_child->scope_index(), i + 1);
      CHECK(first_child->parent().has_value());
      CHECK_EQ(first_child->parent()->scope_index(), i);
    }

    if (auto sibling = scope.next_sibling()) {
      CHECK_GT(i, 0);
      CHECK_GT(sibling->scope_index(), i);
      CHECK_LT(sibling->scope_index(), scope_count);
      CHECK(sibling->parent().has_value());
      CHECK_EQ(sibling->parent()->scope_index(), scope.parent()->scope_index());
    }
  }
}
#endif  // VERIFY_HEAP

}  // namespace internal
}  // namespace v8