/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "debugger/Script-inl.h"

#include "mozilla/Maybe.h"   // for Some, Maybe
#include "mozilla/Span.h"    // for Span
#include "mozilla/Vector.h"  // for Vector

#include <stddef.h>  // for ptrdiff_t
#include <stdint.h>  // for uint32_t, UINT32_MAX, SIZE_MAX, int32_t

#include "jsnum.h"             // for ToNumber
#include "NamespaceImports.h"  // for CallArgs, RootedValue

#include "builtin/Array.h"         // for NewDenseEmptyArray
#include "debugger/Debugger.h"     // for DebuggerScriptReferent, Debugger
#include "debugger/DebugScript.h"  // for DebugScript
#include "debugger/Source.h"       // for DebuggerSource
#include "gc/GC.h"                 // for MemoryUse, MemoryUse::Breakpoint
#include "gc/Tracer.h"         // for TraceManuallyBarrieredCrossCompartmentEdge
#include "gc/Zone.h"           // for Zone
#include "gc/ZoneAllocator.h"  // for AddCellMemory
#include "js/CallArgs.h"       // for CallArgs, CallArgsFromVp
#include "js/ColumnNumber.h"  // JS::LimitedColumnNumberOneOrigin, JS::WasmFunctionIndex
#include "js/friend/ErrorMessages.h"  // for GetErrorMessage, JSMSG_*
#include "js/GCVariant.h"             // for GCVariant
#include "js/HeapAPI.h"               // for GCCellPtr
#include "js/RootingAPI.h"            // for Rooted
#include "js/Wrapper.h"               // for UncheckedUnwrap
#include "vm/ArrayObject.h"           // for ArrayObject
#include "vm/BytecodeUtil.h"          // for GET_JUMP_OFFSET
#include "vm/Compartment.h"           // for JS::Compartment
#include "vm/EnvironmentObject.h"     // for EnvironmentCoordinateNameSlow
#include "vm/GlobalObject.h"          // for GlobalObject
#include "vm/JSContext.h"             // for JSContext, ReportValueError
#include "vm/JSFunction.h"            // for JSFunction
#include "vm/JSObject.h"              // for RequireObject, JSObject
#include "vm/JSScript.h"              // for BaseScript
#include "vm/ObjectOperations.h"      // for DefineDataProperty, HasOwnProperty
#include "vm/PlainObject.h"           // for js::PlainObject
#include "vm/Realm.h"                 // for AutoRealm
#include "vm/Runtime.h"               // for JSAtomState, JSRuntime
#include "vm/StringType.h"            // for NameToId, PropertyName, JSAtom
#include "wasm/WasmDebug.h"           // for ExprLoc, DebugState
#include "wasm/WasmInstance.h"        // for Instance
#include "wasm/WasmJS.h"              // for WasmInstanceObject
#include "wasm/WasmTypeDecls.h"       // for Bytes

#include "gc/Marking-inl.h"       // for MaybeForwardedObjectIs
#include "vm/BytecodeUtil-inl.h"  // for BytecodeRangeWithPosition
#include "vm/JSAtomUtils-inl.h"   // for PrimitiveValueToId
#include "vm/JSObject-inl.h"  // for NewBuiltinClassInstance, NewObjectWithGivenProto, NewTenuredObjectWithGivenProto
#include "vm/JSScript-inl.h"  // for JSScript::global
#include "vm/ObjectOperations-inl.h"  // for GetProperty
#include "vm/Realm-inl.h"             // for AutoRealm::AutoRealm

using namespace js;

using mozilla::Maybe;
using mozilla::Some;

const JSClassOps DebuggerScript::classOps_ = {
    nullptr,                          // addProperty
    nullptr,                          // delProperty
    nullptr,                          // enumerate
    nullptr,                          // newEnumerate
    nullptr,                          // resolve
    nullptr,                          // mayResolve
    nullptr,                          // finalize
    nullptr,                          // call
    nullptr,                          // construct
    CallTraceMethod<DebuggerScript>,  // trace
};

const JSClass DebuggerScript::class_ = {
    "Script",
    JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS),
    &classOps_,
};

void DebuggerScript::trace(JSTracer* trc) {
  // This comes from a private pointer, so no barrier needed.
  gc::Cell* cell = getReferentCell();
  if (cell) {
    if (cell->is<BaseScript>()) {
      BaseScript* script = cell->as<BaseScript>();
      TraceManuallyBarrieredCrossCompartmentEdge(
          trc, this, &script, "Debugger.Script script referent");
      if (script != cell->as<BaseScript>()) {
        setReservedSlotGCThingAsPrivateUnbarriered(SCRIPT_SLOT, script);
      }
    } else {
      JSObject* wasm = cell->as<JSObject>();
      TraceManuallyBarrieredCrossCompartmentEdge(
          trc, this, &wasm, "Debugger.Script wasm referent");
      if (wasm != cell->as<JSObject>()) {
        MOZ_ASSERT(gc::MaybeForwardedObjectIs<WasmInstanceObject>(wasm));
        setReservedSlotGCThingAsPrivateUnbarriered(SCRIPT_SLOT, wasm);
      }
    }
  }
}

/* static */
NativeObject* DebuggerScript::initClass(JSContext* cx,
                                        Handle<GlobalObject*> global,
                                        HandleObject debugCtor) {
  return InitClass(cx, debugCtor, nullptr, nullptr, "Script", construct, 0,
                   properties_, methods_, nullptr, nullptr);
}

/* static */
DebuggerScript* DebuggerScript::create(JSContext* cx, HandleObject proto,
                                       Handle<DebuggerScriptReferent> referent,
                                       Handle<NativeObject*> debugger) {
  DebuggerScript* scriptobj =
      NewTenuredObjectWithGivenProto<DebuggerScript>(cx, proto);
  if (!scriptobj) {
    return nullptr;
  }

  scriptobj->setReservedSlot(DebuggerScript::OWNER_SLOT,
                             ObjectValue(*debugger));
  referent.get().match([&](auto& scriptHandle) {
    scriptobj->setReservedSlotGCThingAsPrivate(SCRIPT_SLOT, scriptHandle);
  });

  return scriptobj;
}

static JSScript* DelazifyScript(JSContext* cx, Handle<BaseScript*> script) {
  if (script->hasBytecode()) {
    return script->asJSScript();
  }
  MOZ_ASSERT(script->isFunction());

  // JSFunction::getOrCreateScript requires an enclosing scope. This requires
  // the enclosing script to be non-lazy.
  if (script->hasEnclosingScript()) {
    Rooted<BaseScript*> enclosingScript(cx, script->enclosingScript());
    if (!DelazifyScript(cx, enclosingScript)) {
      return nullptr;
    }

    if (!script->isReadyForDelazification()) {
      // It didn't work! Delazifying the enclosing script still didn't
      // delazify this script. This happens when the function
      // corresponding to this script was removed by constant folding.
      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_OPTIMIZED_OUT_FUN);
      return nullptr;
    }
  }

  MOZ_ASSERT(script->enclosingScope());

  RootedFunction fun(cx, script->function());
  AutoRealm ar(cx, fun);
  return JSFunction::getOrCreateScript(cx, fun);
}

/* static */
DebuggerScript* DebuggerScript::check(JSContext* cx, HandleValue v) {
  JSObject* thisobj = RequireObject(cx, v);
  if (!thisobj) {
    return nullptr;
  }
  if (!thisobj->is<DebuggerScript>()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_INCOMPATIBLE_PROTO, "Debugger.Script",
                              "method", thisobj->getClass()->name);
    return nullptr;
  }

  return &thisobj->as<DebuggerScript>();
}

struct MOZ_STACK_CLASS DebuggerScript::CallData {
  JSContext* cx;
  const CallArgs& args;

  Handle<DebuggerScript*> obj;
  Rooted<DebuggerScriptReferent> referent;
  RootedScript script;

  CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerScript*> obj)
      : cx(cx),
        args(args),
        obj(obj),
        referent(cx, obj->getReferent()),
        script(cx) {}

  [[nodiscard]] bool ensureScriptMaybeLazy() {
    if (!referent.is<BaseScript*>()) {
      ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK,
                       args.thisv(), nullptr, "a JS script");
      return false;
    }
    return true;
  }

  [[nodiscard]] bool ensureScript() {
    if (!ensureScriptMaybeLazy()) {
      return false;
    }
    script = DelazifyScript(cx, referent.as<BaseScript*>());
    if (!script) {
      return false;
    }
    return true;
  }

  bool getIsGeneratorFunction();
  bool getIsAsyncFunction();
  bool getIsFunction();
  bool getIsModule();
  bool getDisplayName();
  bool getParameterNames();
  bool getUrl();
  bool getStartLine();
  bool getStartColumn();
  bool getLineCount();
  bool getSource();
  bool getSourceStart();
  bool getSourceLength();
  bool getMainOffset();
  bool getGlobal();
  bool getFormat();
  bool getChildScripts();
  bool getPossibleBreakpoints();
  bool getPossibleBreakpointOffsets();
  bool getOffsetMetadata();
  bool getOffsetLocation();
  bool getEffectfulOffsets();
  bool getAllOffsets();
  bool getAllColumnOffsets();
  bool getLineOffsets();
  bool setBreakpoint();
  bool getBreakpoints();
  bool clearBreakpoint();
  bool clearAllBreakpoints();
  bool isInCatchScope();
  bool getOffsetsCoverage();

  using Method = bool (CallData::*)();

  template <Method MyMethod>
  static bool ToNative(JSContext* cx, unsigned argc, Value* vp);
};

template <DebuggerScript::CallData::Method MyMethod>
/* static */
bool DebuggerScript::CallData::ToNative(JSContext* cx, unsigned argc,
                                        Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  Rooted<DebuggerScript*> obj(cx, DebuggerScript::check(cx, args.thisv()));
  if (!obj) {
    return false;
  }

  CallData data(cx, args, obj);
  return (data.*MyMethod)();
}

bool DebuggerScript::CallData::getIsGeneratorFunction() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  args.rval().setBoolean(obj->getReferentScript()->isGenerator());
  return true;
}

bool DebuggerScript::CallData::getIsAsyncFunction() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  args.rval().setBoolean(obj->getReferentScript()->isAsync());
  return true;
}

bool DebuggerScript::CallData::getIsFunction() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }

  args.rval().setBoolean(obj->getReferentScript()->function());
  return true;
}

bool DebuggerScript::CallData::getIsModule() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  BaseScript* script = referent.as<BaseScript*>();

  args.rval().setBoolean(script->isModule());
  return true;
}

bool DebuggerScript::CallData::getDisplayName() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }

  JSFunction* func = obj->getReferentScript()->function();
  if (!func) {
    args.rval().setUndefined();
    return true;
  }

  JSAtom* name = func->fullDisplayAtom();
  if (!name) {
    args.rval().setUndefined();
    return true;
  }

  RootedValue namev(cx, StringValue(name));
  Debugger* dbg = obj->owner();
  if (!dbg->wrapDebuggeeValue(cx, &namev)) {
    return false;
  }
  args.rval().set(namev);
  return true;
}

bool DebuggerScript::CallData::getParameterNames() {
  if (!ensureScript()) {
    return false;
  }

  RootedFunction fun(cx, referent.as<BaseScript*>()->function());
  if (!fun) {
    args.rval().setUndefined();
    return true;
  }

  ArrayObject* arr = GetFunctionParameterNamesArray(cx, fun);
  if (!arr) {
    return false;
  }

  args.rval().setObject(*arr);
  return true;
}

bool DebuggerScript::CallData::getUrl() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }

  Rooted<BaseScript*> script(cx, referent.as<BaseScript*>());

  if (script->filename()) {
    JSString* str;
    if (const char* introducer = script->scriptSource()->introducerFilename()) {
      str =
          NewStringCopyUTF8N(cx, JS::UTF8Chars(introducer, strlen(introducer)));
    } else {
      const char* filename = script->filename();
      str = NewStringCopyUTF8N(cx, JS::UTF8Chars(filename, strlen(filename)));
    }
    if (!str) {
      return false;
    }
    args.rval().setString(str);
  } else {
    args.rval().setNull();
  }
  return true;
}

bool DebuggerScript::CallData::getStartLine() {
  args.rval().setNumber(
      referent.get().match([](BaseScript*& s) { return s->lineno(); },
                           [](WasmInstanceObject*&) { return (uint32_t)1; }));
  return true;
}

bool DebuggerScript::CallData::getStartColumn() {
  JS::LimitedColumnNumberOneOrigin column = referent.get().match(
      [](BaseScript*& s) { return s->column(); },
      [](WasmInstanceObject*&) {
        return JS::LimitedColumnNumberOneOrigin(
            JS::WasmFunctionIndex::DefaultBinarySourceColumnNumberOneOrigin);
      });
  args.rval().setNumber(column.oneOriginValue());
  return true;
}

struct DebuggerScript::GetLineCountMatcher {
  JSContext* cx_;
  double totalLines;

  explicit GetLineCountMatcher(JSContext* cx) : cx_(cx), totalLines(0.0) {}
  using ReturnType = bool;

  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }
    totalLines = double(GetScriptLineExtent(script));
    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    if (instance.debugEnabled()) {
      totalLines = double(instance.debug().bytecode().length());
    } else {
      totalLines = 0;
    }
    return true;
  }
};

bool DebuggerScript::CallData::getLineCount() {
  GetLineCountMatcher matcher(cx);
  if (!referent.match(matcher)) {
    return false;
  }
  args.rval().setNumber(matcher.totalLines);
  return true;
}

class DebuggerScript::GetSourceMatcher {
  JSContext* cx_;
  Debugger* dbg_;

 public:
  GetSourceMatcher(JSContext* cx, Debugger* dbg) : cx_(cx), dbg_(dbg) {}

  using ReturnType = DebuggerSource*;

  ReturnType match(Handle<BaseScript*> script) {
    Rooted<ScriptSourceObject*> source(cx_, script->sourceObject());
    return dbg_->wrapSource(cx_, source);
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    return dbg_->wrapWasmSource(cx_, wasmInstance);
  }
};

bool DebuggerScript::CallData::getSource() {
  Debugger* dbg = obj->owner();

  GetSourceMatcher matcher(cx, dbg);
  Rooted<DebuggerSource*> sourceObject(cx, referent.match(matcher));
  if (!sourceObject) {
    return false;
  }

  args.rval().setObject(*sourceObject);
  return true;
}

bool DebuggerScript::CallData::getSourceStart() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  args.rval().setNumber(uint32_t(obj->getReferentScript()->sourceStart()));
  return true;
}

bool DebuggerScript::CallData::getSourceLength() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  args.rval().setNumber(uint32_t(obj->getReferentScript()->sourceLength()));
  return true;
}

bool DebuggerScript::CallData::getMainOffset() {
  if (!ensureScript()) {
    return false;
  }
  args.rval().setNumber(uint32_t(script->mainOffset()));
  return true;
}

bool DebuggerScript::CallData::getGlobal() {
  if (!ensureScript()) {
    return false;
  }
  Debugger* dbg = obj->owner();

  RootedValue v(cx, ObjectValue(script->global()));
  if (!dbg->wrapDebuggeeValue(cx, &v)) {
    return false;
  }
  args.rval().set(v);
  return true;
}

bool DebuggerScript::CallData::getFormat() {
  args.rval().setString(referent.get().match(
      [this](BaseScript*&) { return cx->names().js.get(); },
      [this](WasmInstanceObject*&) { return cx->names().wasm.get(); }));
  return true;
}

static bool PushFunctionScript(JSContext* cx, Debugger* dbg, HandleFunction fun,
                               HandleObject array) {
  // Ignore asm.js natives.
  if (!IsInterpretedNonSelfHostedFunction(fun)) {
    return true;
  }

  Rooted<BaseScript*> script(cx, fun->baseScript());
  MOZ_ASSERT(script);
  if (!script) {
    // If the function doesn't have script, ignore it.
    return true;
  }
  RootedObject wrapped(cx, dbg->wrapScript(cx, script));
  if (!wrapped) {
    return false;
  }

  return NewbornArrayPush(cx, array, ObjectValue(*wrapped));
}

static bool PushInnerFunctions(JSContext* cx, Debugger* dbg, HandleObject array,
                               mozilla::Span<const JS::GCCellPtr> gcThings) {
  RootedFunction fun(cx);

  for (JS::GCCellPtr gcThing : gcThings) {
    if (!gcThing.is<JSObject>()) {
      continue;
    }

    JSObject* obj = &gcThing.as<JSObject>();
    if (obj->is<JSFunction>()) {
      fun = &obj->as<JSFunction>();

      // Ignore any delazification placeholder functions. These should not be
      // exposed to debugger in any way.
      if (fun->isGhost()) {
        continue;
      }

      if (!PushFunctionScript(cx, dbg, fun, array)) {
        return false;
      }
    }
  }

  return true;
}

bool DebuggerScript::CallData::getChildScripts() {
  if (!ensureScriptMaybeLazy()) {
    return false;
  }
  Debugger* dbg = obj->owner();

  RootedObject result(cx, NewDenseEmptyArray(cx));
  if (!result) {
    return false;
  }

  Rooted<BaseScript*> script(cx, obj->getReferent().as<BaseScript*>());
  if (!PushInnerFunctions(cx, dbg, result, script->gcthings())) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

static bool ScriptOffset(JSContext* cx, const Value& v, size_t* offsetp) {
  double d;
  size_t off;

  bool ok = v.isNumber();
  if (ok) {
    d = v.toNumber();
    off = size_t(d);
  }
  if (!ok || off != d) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_DEBUG_BAD_OFFSET);
    return false;
  }
  *offsetp = off;
  return true;
}

static bool EnsureScriptOffsetIsValid(JSContext* cx, JSScript* script,
                                      size_t offset) {
  if (IsValidBytecodeOffset(cx, script, offset)) {
    return true;
  }
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                            JSMSG_DEBUG_BAD_OFFSET);
  return false;
}

static bool IsGeneratorSlotInitialization(JSScript* script, size_t offset,
                                          JSContext* cx) {
  jsbytecode* pc = script->offsetToPC(offset);
  if (JSOp(*pc) != JSOp::SetAliasedVar) {
    return false;
  }

  PropertyName* name = EnvironmentCoordinateNameSlow(script, pc);
  return name == cx->names().dot_generator_;
}

static bool EnsureBreakpointIsAllowed(JSContext* cx, JSScript* script,
                                      size_t offset) {
  // Disallow breakpoint for `JSOp::SetAliasedVar` after `JSOp::Generator`.
  // Those 2 instructions are supposed to be atomic, and nothing should happen
  // in between them.
  //
  // Hitting a breakpoint there breaks the assumption around the existence of
  // the frame's `GeneratorInfo`.
  // (see `DebugAPI::slowPathOnNewGenerator` and `DebuggerFrame::create`)
  if (IsGeneratorSlotInitialization(script, offset, cx)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_DEBUG_BREAKPOINT_NOT_ALLOWED);
    return false;
  }

  return true;
}

template <bool OnlyOffsets>
class DebuggerScript::GetPossibleBreakpointsMatcher {
  JSContext* cx_;
  MutableHandleObject result_;

  Maybe<size_t> minOffset;
  Maybe<size_t> maxOffset;

  Maybe<uint32_t> minLine;
  JS::LimitedColumnNumberOneOrigin minColumn;
  Maybe<uint32_t> maxLine;
  JS::LimitedColumnNumberOneOrigin maxColumn;

  bool passesQuery(size_t offset, uint32_t lineno,
                   JS::LimitedColumnNumberOneOrigin colno) {
    // [minOffset, maxOffset) - Inclusive minimum and exclusive maximum.
    if ((minOffset && offset < *minOffset) ||
        (maxOffset && offset >= *maxOffset)) {
      return false;
    }

    if (minLine) {
      if (lineno < *minLine || (lineno == *minLine && colno < minColumn)) {
        return false;
      }
    }

    if (maxLine) {
      if (lineno > *maxLine || (lineno == *maxLine && colno >= maxColumn)) {
        return false;
      }
    }

    return true;
  }

  bool maybeAppendEntry(size_t offset, uint32_t lineno,
                        JS::LimitedColumnNumberOneOrigin colno,
                        bool isStepStart) {
    if (!passesQuery(offset, lineno, colno)) {
      return true;
    }

    if (OnlyOffsets) {
      if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) {
        return false;
      }

      return true;
    }

    Rooted<PlainObject*> entry(cx_, NewPlainObject(cx_));
    if (!entry) {
      return false;
    }

    RootedValue value(cx_, NumberValue(offset));
    if (!DefineDataProperty(cx_, entry, cx_->names().offset, value)) {
      return false;
    }

    value = NumberValue(lineno);
    if (!DefineDataProperty(cx_, entry, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(colno.oneOriginValue());
    if (!DefineDataProperty(cx_, entry, cx_->names().columnNumber, value)) {
      return false;
    }

    value = BooleanValue(isStepStart);
    if (!DefineDataProperty(cx_, entry, cx_->names().isStepStart, value)) {
      return false;
    }

    if (!NewbornArrayPush(cx_, result_, ObjectValue(*entry))) {
      return false;
    }
    return true;
  }

  template <typename T>
  bool parseIntValueImpl(HandleValue value, T* result) {
    if (!value.isNumber()) {
      return false;
    }

    double doubleOffset = value.toNumber();
    if (doubleOffset < 0 || (unsigned int)doubleOffset != doubleOffset) {
      return false;
    }

    *result = doubleOffset;
    return true;
  }

  bool parseUint32Value(HandleValue value, uint32_t* result) {
    return parseIntValueImpl(value, result);
  }
  bool parseColumnValue(HandleValue value,
                        JS::LimitedColumnNumberOneOrigin* result) {
    uint32_t tmp;
    if (!parseIntValueImpl(value, &tmp)) {
      return false;
    }
    if (tmp == 0) {
      return false;
    }
    *result->addressOfValueForTranscode() = tmp;
    if (!result->valid()) {
      return false;
    }
    return true;
  }
  bool parseSizeTValue(HandleValue value, size_t* result) {
    return parseIntValueImpl(value, result);
  }

  template <typename T>
  bool parseIntValueMaybeImpl(HandleValue value, Maybe<T>* result) {
    T result_;
    if (!parseIntValueImpl(value, &result_)) {
      return false;
    }

    *result = Some(result_);
    return true;
  }

  bool parseUint32Value(HandleValue value, Maybe<uint32_t>* result) {
    return parseIntValueMaybeImpl(value, result);
  }
  bool parseSizeTValue(HandleValue value, Maybe<size_t>* result) {
    return parseIntValueMaybeImpl(value, result);
  }

 public:
  explicit GetPossibleBreakpointsMatcher(JSContext* cx,
                                         MutableHandleObject result)
      : cx_(cx), result_(result) {}

  bool parseQuery(HandleObject query) {
    RootedValue lineValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().line, &lineValue)) {
      return false;
    }

    RootedValue minLineValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().minLine, &minLineValue)) {
      return false;
    }

    RootedValue minColumnValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().minColumn,
                     &minColumnValue)) {
      return false;
    }

    RootedValue minOffsetValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().minOffset,
                     &minOffsetValue)) {
      return false;
    }

    RootedValue maxLineValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().maxLine, &maxLineValue)) {
      return false;
    }

    RootedValue maxColumnValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().maxColumn,
                     &maxColumnValue)) {
      return false;
    }

    RootedValue maxOffsetValue(cx_);
    if (!GetProperty(cx_, query, query, cx_->names().maxOffset,
                     &maxOffsetValue)) {
      return false;
    }

    if (!minOffsetValue.isUndefined()) {
      if (!parseSizeTValue(minOffsetValue, &minOffset)) {
        JS_ReportErrorNumberASCII(
            cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
            "getPossibleBreakpoints' 'minOffset'", "not an integer");
        return false;
      }
    }
    if (!maxOffsetValue.isUndefined()) {
      if (!parseSizeTValue(maxOffsetValue, &maxOffset)) {
        JS_ReportErrorNumberASCII(
            cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
            "getPossibleBreakpoints' 'maxOffset'", "not an integer");
        return false;
      }
    }

    if (!lineValue.isUndefined()) {
      if (!minLineValue.isUndefined() || !maxLineValue.isUndefined()) {
        JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                  JSMSG_UNEXPECTED_TYPE,
                                  "getPossibleBreakpoints' 'line'",
                                  "not allowed alongside 'minLine'/'maxLine'");
        return false;
      }

      uint32_t line;
      if (!parseUint32Value(lineValue, &line)) {
        JS_ReportErrorNumberASCII(
            cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
            "getPossibleBreakpoints' 'line'", "not an integer");
        return false;
      }

      // If no end column is given, we use the default of 0 and wrap to
      // the next line.
      minLine = Some(line);
      maxLine = Some(line + (maxColumnValue.isUndefined() ? 1 : 0));
    }

    if (!minLineValue.isUndefined()) {
      if (!parseUint32Value(minLineValue, &minLine)) {
        JS_ReportErrorNumberASCII(
            cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
            "getPossibleBreakpoints' 'minLine'", "not an integer");
        return false;
      }
    }

    if (!minColumnValue.isUndefined()) {
      if (!minLine) {
        JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                  JSMSG_UNEXPECTED_TYPE,
                                  "getPossibleBreakpoints' 'minColumn'",
                                  "not allowed without 'line' or 'minLine'");
        return false;
      }

      if (!parseColumnValue(minColumnValue, &minColumn)) {
        JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                  JSMSG_UNEXPECTED_TYPE,
                                  "getPossibleBreakpoints' 'minColumn'",
                                  "not a positive integer in valid range");
        return false;
      }
    }

    if (!maxLineValue.isUndefined()) {
      if (!parseUint32Value(maxLineValue, &maxLine)) {
        JS_ReportErrorNumberASCII(
            cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
            "getPossibleBreakpoints' 'maxLine'", "not an integer");
        return false;
      }
    }

    if (!maxColumnValue.isUndefined()) {
      if (!maxLine) {
        JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                  JSMSG_UNEXPECTED_TYPE,
                                  "getPossibleBreakpoints' 'maxColumn'",
                                  "not allowed without 'line' or 'maxLine'");
        return false;
      }

      if (!parseColumnValue(maxColumnValue, &maxColumn)) {
        JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                  JSMSG_UNEXPECTED_TYPE,
                                  "getPossibleBreakpoints' 'maxColumn'",
                                  "not a positive integer in valid range");
        return false;
      }
    }

    return true;
  }

  using ReturnType = bool;
  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    // Second pass: build the result array.
    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) {
      if (!r.frontIsBreakablePoint()) {
        continue;
      }

      size_t offset = r.frontOffset();
      uint32_t lineno = r.frontLineNumber();
      JS::LimitedColumnNumberOneOrigin colno = r.frontColumnNumber();

      if (!maybeAppendEntry(offset, lineno, colno,
                            r.frontIsBreakableStepPoint())) {
        return false;
      }
    }

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();

    Vector<wasm::ExprLoc> offsets(cx_);
    if (instance.debugEnabled() &&
        !instance.debug().getAllColumnOffsets(&offsets)) {
      return false;
    }

    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    for (uint32_t i = 0; i < offsets.length(); i++) {
      uint32_t lineno = offsets[i].lineno;
      JS::LimitedColumnNumberOneOrigin column(offsets[i].column);
      size_t offset = offsets[i].offset;
      if (!maybeAppendEntry(offset, lineno, column, true)) {
        return false;
      }
    }
    return true;
  }
};

bool DebuggerScript::CallData::getPossibleBreakpoints() {
  RootedObject result(cx);
  GetPossibleBreakpointsMatcher<false> matcher(cx, &result);
  if (args.length() >= 1 && !args[0].isUndefined()) {
    RootedObject queryObject(cx, RequireObject(cx, args[0]));
    if (!queryObject || !matcher.parseQuery(queryObject)) {
      return false;
    }
  }
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

bool DebuggerScript::CallData::getPossibleBreakpointOffsets() {
  RootedObject result(cx);
  GetPossibleBreakpointsMatcher<true> matcher(cx, &result);
  if (args.length() >= 1 && !args[0].isUndefined()) {
    RootedObject queryObject(cx, RequireObject(cx, args[0]));
    if (!queryObject || !matcher.parseQuery(queryObject)) {
      return false;
    }
  }
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

class DebuggerScript::GetOffsetMetadataMatcher {
  JSContext* cx_;
  size_t offset_;
  MutableHandle<PlainObject*> result_;

 public:
  explicit GetOffsetMetadataMatcher(JSContext* cx, size_t offset,
                                    MutableHandle<PlainObject*> result)
      : cx_(cx), offset_(offset), result_(result) {}
  using ReturnType = bool;
  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
      return false;
    }

    result_.set(NewPlainObject(cx_));
    if (!result_) {
      return false;
    }

    BytecodeRangeWithPosition r(cx_, script);
    while (!r.empty() && r.frontOffset() < offset_) {
      r.popFront();
    }

    RootedValue value(cx_, NumberValue(r.frontLineNumber()));
    if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(r.frontColumnNumber().oneOriginValue());
    if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
      return false;
    }

    value = BooleanValue(r.frontIsBreakablePoint());
    if (!DefineDataProperty(cx_, result_, cx_->names().isBreakpoint, value)) {
      return false;
    }

    value = BooleanValue(r.frontIsBreakableStepPoint());
    if (!DefineDataProperty(cx_, result_, cx_->names().isStepStart, value)) {
      return false;
    }

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    if (!instance.debugEnabled()) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_OFFSET);
      return false;
    }

    uint32_t lineno;
    JS::LimitedColumnNumberOneOrigin column;
    if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_OFFSET);
      return false;
    }

    result_.set(NewPlainObject(cx_));
    if (!result_) {
      return false;
    }

    RootedValue value(cx_, NumberValue(lineno));
    if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(column.oneOriginValue());
    if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
      return false;
    }

    value.setBoolean(true);
    if (!DefineDataProperty(cx_, result_, cx_->names().isBreakpoint, value)) {
      return false;
    }

    value.setBoolean(true);
    if (!DefineDataProperty(cx_, result_, cx_->names().isStepStart, value)) {
      return false;
    }

    return true;
  }
};

bool DebuggerScript::CallData::getOffsetMetadata() {
  if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetMetadata", 1)) {
    return false;
  }
  size_t offset;
  if (!ScriptOffset(cx, args[0], &offset)) {
    return false;
  }

  Rooted<PlainObject*> result(cx);
  GetOffsetMetadataMatcher matcher(cx, offset, &result);
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

namespace {

/*
 * FlowGraphSummary::populate(cx, script) computes a summary of script's
 * control flow graph used by DebuggerScript_{getAllOffsets,getLineOffsets}.
 *
 * An instruction on a given line is an entry point for that line if it can be
 * reached from (an instruction on) a different line. We distinguish between the
 * following cases:
 *   - hasNoEdges:
 *       The instruction cannot be reached, so the instruction is not an entry
 *       point for the line it is on.
 *   - hasSingleEdge:
 *       The instruction can be reached from a single line. If this line is
 *       different from the line the instruction is on, the instruction is an
 *       entry point for that line.
 *
 * Similarly, an instruction on a given position (line/column pair) is an
 * entry point for that position if it can be reached from (an instruction on) a
 * different position. Again, we distinguish between the following cases:
 *   - hasNoEdges:
 *       The instruction cannot be reached, so the instruction is not an entry
 *       point for the position it is on.
 *   - hasSingleEdge:
 *       The instruction can be reached from a single position. If this line is
 *       different from the position the instruction is on, the instruction is
 *       an entry point for that position.
 */
class FlowGraphSummary {
 public:
  class Entry {
   public:
    static constexpr uint32_t Line_HasNoEdge = UINT32_MAX;
    static constexpr uint32_t Column_HasMultipleEdge = UINT32_MAX;

    // NOTE: column can be Column_HasMultipleEdge.
    static Entry createWithSingleEdgeOrMultipleEdge(uint32_t lineno,
                                                    uint32_t column) {
      return Entry(lineno, column);
    }

    static Entry createWithMultipleEdgesFromSingleLine(uint32_t lineno) {
      return Entry(lineno, Column_HasMultipleEdge);
    }

    static Entry createWithMultipleEdgesFromMultipleLines() {
      return Entry(Line_HasNoEdge, Column_HasMultipleEdge);
    }

    Entry() : lineno_(Line_HasNoEdge), column_(1) {}

    bool hasNoEdges() const {
      return lineno_ == Line_HasNoEdge && column_ != Column_HasMultipleEdge;
    }

    bool hasSingleEdge() const {
      return lineno_ != Line_HasNoEdge && column_ != Column_HasMultipleEdge;
    }

    uint32_t lineno() const { return lineno_; }

    // Returns 1-origin column number or the sentinel value
    // Column_HasMultipleEdge.
    uint32_t columnOrSentinel() const { return column_; }

    JS::LimitedColumnNumberOneOrigin column() const {
      MOZ_ASSERT(column_ != Column_HasMultipleEdge);
      return JS::LimitedColumnNumberOneOrigin(column_);
    }

   private:
    Entry(uint32_t lineno, uint32_t column)
        : lineno_(lineno), column_(column) {}

    // Line number (1-origin).
    // Line_HasNoEdge for no edge.
    uint32_t lineno_;

    // Column number in UTF-16 code units (1-origin).
    // Column_HasMultipleEdge for multiple edge.
    uint32_t column_;
  };

  explicit FlowGraphSummary(JSContext* cx) : entries_(cx) {}

  Entry& operator[](size_t index) { return entries_[index]; }

  bool populate(JSContext* cx, JSScript* script) {
    if (!entries_.growBy(script->length())) {
      return false;
    }
    unsigned mainOffset = script->pcToOffset(script->main());
    entries_[mainOffset] = Entry::createWithMultipleEdgesFromMultipleLines();

    // The following code uses uint32_t for column numbers.
    // The value is either 1-origin column number,
    // or Entry::Column_HasMultipleEdge.

    uint32_t prevLineno = script->lineno();
    uint32_t prevColumn = 1;
    JSOp prevOp = JSOp::Nop;
    for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
      uint32_t lineno = prevLineno;
      uint32_t column = prevColumn;
      JSOp op = r.frontOpcode();

      if (BytecodeFallsThrough(prevOp)) {
        addEdge(prevLineno, prevColumn, r.frontOffset());
      }

      // If we visit the branch target before we visit the
      // branch op itself, just reuse the previous location.
      // This is reasonable for the time being because this
      // situation can currently only arise from loop heads,
      // where this assumption holds.
      if (BytecodeIsJumpTarget(op) && !entries_[r.frontOffset()].hasNoEdges()) {
        lineno = entries_[r.frontOffset()].lineno();
        column = entries_[r.frontOffset()].columnOrSentinel();
      }

      if (r.frontIsEntryPoint()) {
        lineno = r.frontLineNumber();
        column = r.frontColumnNumber().oneOriginValue();
      }

      if (IsJumpOpcode(op)) {
        addEdge(lineno, column, r.frontOffset() + GET_JUMP_OFFSET(r.frontPC()));
      } else if (op == JSOp::TableSwitch) {
        jsbytecode* const switchPC = r.frontPC();
        jsbytecode* pc = switchPC;
        size_t offset = r.frontOffset();
        ptrdiff_t step = JUMP_OFFSET_LEN;
        size_t defaultOffset = offset + GET_JUMP_OFFSET(pc);
        pc += step;
        addEdge(lineno, column, defaultOffset);

        int32_t low = GET_JUMP_OFFSET(pc);
        pc += JUMP_OFFSET_LEN;
        int ncases = GET_JUMP_OFFSET(pc) - low + 1;
        pc += JUMP_OFFSET_LEN;

        for (int i = 0; i < ncases; i++) {
          size_t target = script->tableSwitchCaseOffset(switchPC, i);
          addEdge(lineno, column, target);
        }
      } else if (op == JSOp::Try) {
        // As there is no literal incoming edge into the catch block, we
        // make a fake one by copying the JSOp::Try location, as-if this
        // was an incoming edge of the catch block. This is needed
        // because we only report offsets of entry points which have
        // valid incoming edges.
        for (const TryNote& tn : script->trynotes()) {
          if (tn.start == r.frontOffset() + JSOpLength_Try) {
            uint32_t catchOffset = tn.start + tn.length;
            if (tn.kind() == TryNoteKind::Catch ||
                tn.kind() == TryNoteKind::Finally) {
              addEdge(lineno, column, catchOffset);
            }
          }
        }
      }

      prevLineno = lineno;
      prevColumn = column;
      prevOp = op;
    }

    return true;
  }

 private:
  // sourceColumn is either 1-origin column number,
  // or Entry::Column_HasMultipleEdge.
  void addEdge(uint32_t sourceLineno, uint32_t sourceColumn,
               size_t targetOffset) {
    if (entries_[targetOffset].hasNoEdges()) {
      entries_[targetOffset] =
          Entry::createWithSingleEdgeOrMultipleEdge(sourceLineno, sourceColumn);
    } else if (entries_[targetOffset].lineno() != sourceLineno) {
      entries_[targetOffset] =
          Entry::createWithMultipleEdgesFromMultipleLines();
    } else if (entries_[targetOffset].columnOrSentinel() != sourceColumn) {
      entries_[targetOffset] =
          Entry::createWithMultipleEdgesFromSingleLine(sourceLineno);
    }
  }

  Vector<Entry> entries_;
};

} /* anonymous namespace */

class DebuggerScript::GetOffsetLocationMatcher {
  JSContext* cx_;
  size_t offset_;
  MutableHandle<PlainObject*> result_;

 public:
  explicit GetOffsetLocationMatcher(JSContext* cx, size_t offset,
                                    MutableHandle<PlainObject*> result)
      : cx_(cx), offset_(offset), result_(result) {}
  using ReturnType = bool;
  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
      return false;
    }

    FlowGraphSummary flowData(cx_);
    if (!flowData.populate(cx_, script)) {
      return false;
    }

    result_.set(NewPlainObject(cx_));
    if (!result_) {
      return false;
    }

    BytecodeRangeWithPosition r(cx_, script);
    while (!r.empty() && r.frontOffset() < offset_) {
      r.popFront();
    }

    size_t offset = r.frontOffset();
    bool isEntryPoint = r.frontIsEntryPoint();

    // Line numbers are only correctly defined on entry points. Thus looks
    // either for the next valid offset in the flowData, being the last entry
    // point flowing into the current offset, or for the next valid entry point.
    while (!r.frontIsEntryPoint() &&
           !flowData[r.frontOffset()].hasSingleEdge()) {
      r.popFront();
      MOZ_ASSERT(!r.empty());
    }

    // If this is an entry point, take the line number associated with the entry
    // point, otherwise settle on the next instruction and take the incoming
    // edge position.
    uint32_t lineno;
    JS::LimitedColumnNumberOneOrigin column;
    if (r.frontIsEntryPoint()) {
      lineno = r.frontLineNumber();
      column = r.frontColumnNumber();
    } else {
      MOZ_ASSERT(flowData[r.frontOffset()].hasSingleEdge());
      lineno = flowData[r.frontOffset()].lineno();
      column = flowData[r.frontOffset()].column();
    }

    RootedValue value(cx_, NumberValue(lineno));
    if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(column.oneOriginValue());
    if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
      return false;
    }

    // The same entry point test that is used by getAllColumnOffsets.
    isEntryPoint = (isEntryPoint && !flowData[offset].hasNoEdges() &&
                    (flowData[offset].lineno() != r.frontLineNumber() ||
                     flowData[offset].columnOrSentinel() !=
                         r.frontColumnNumber().oneOriginValue()));
    value.setBoolean(isEntryPoint);
    if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) {
      return false;
    }

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    if (!instance.debugEnabled()) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_OFFSET);
      return false;
    }

    uint32_t lineno;
    JS::LimitedColumnNumberOneOrigin column;
    if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_OFFSET);
      return false;
    }

    result_.set(NewPlainObject(cx_));
    if (!result_) {
      return false;
    }

    RootedValue value(cx_, NumberValue(lineno));
    if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(column.oneOriginValue());
    if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
      return false;
    }

    value.setBoolean(true);
    if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) {
      return false;
    }

    return true;
  }
};

bool DebuggerScript::CallData::getOffsetLocation() {
  if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1)) {
    return false;
  }
  size_t offset;
  if (!ScriptOffset(cx, args[0], &offset)) {
    return false;
  }

  Rooted<PlainObject*> result(cx);
  GetOffsetLocationMatcher matcher(cx, offset, &result);
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

// Return whether an opcode is considered effectful: it can have direct side
// effects that can be observed outside of the current frame. Opcodes are not
// effectful if they only modify the current frame's state, modify objects
// created by the current frame, or can potentially call other scripts or
// natives which could have side effects.
static bool BytecodeIsEffectful(JSScript* script, size_t offset) {
  jsbytecode* pc = script->offsetToPC(offset);
  JSOp op = JSOp(*pc);
  switch (op) {
    case JSOp::SetProp:
    case JSOp::StrictSetProp:
    case JSOp::SetPropSuper:
    case JSOp::StrictSetPropSuper:
    case JSOp::SetElem:
    case JSOp::StrictSetElem:
    case JSOp::SetElemSuper:
    case JSOp::StrictSetElemSuper:
    case JSOp::SetName:
    case JSOp::StrictSetName:
    case JSOp::SetGName:
    case JSOp::StrictSetGName:
    case JSOp::DelProp:
    case JSOp::StrictDelProp:
    case JSOp::DelElem:
    case JSOp::StrictDelElem:
    case JSOp::DelName:
    case JSOp::SetAliasedVar:
    case JSOp::InitHomeObject:
    case JSOp::SetIntrinsic:
    case JSOp::InitGLexical:
    case JSOp::GlobalOrEvalDeclInstantiation:
    case JSOp::SetFunName:
    case JSOp::MutateProto:
    case JSOp::DynamicImport:
    case JSOp::InitialYield:
    case JSOp::Yield:
    case JSOp::Await:
    case JSOp::CanSkipAwait:
#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT
    case JSOp::AddDisposable:
#endif
      return true;

    case JSOp::Nop:
    case JSOp::NopDestructuring:
    case JSOp::NopIsAssignOp:
    case JSOp::TryDestructuring:
    case JSOp::Lineno:
    case JSOp::JumpTarget:
    case JSOp::Undefined:
    case JSOp::JumpIfTrue:
    case JSOp::JumpIfFalse:
    case JSOp::Return:
    case JSOp::RetRval:
    case JSOp::And:
    case JSOp::Or:
    case JSOp::Coalesce:
    case JSOp::Try:
    case JSOp::Throw:
    case JSOp::ThrowWithStack:
#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT
    case JSOp::TakeDisposeCapability:
    case JSOp::CreateSuppressedError:
#endif
    case JSOp::Goto:
    case JSOp::TableSwitch:
    case JSOp::Case:
    case JSOp::Default:
    case JSOp::BitNot:
    case JSOp::BitAnd:
    case JSOp::BitOr:
    case JSOp::BitXor:
    case JSOp::Lsh:
    case JSOp::Rsh:
    case JSOp::Ursh:
    case JSOp::Add:
    case JSOp::Sub:
    case JSOp::Mul:
    case JSOp::Div:
    case JSOp::Mod:
    case JSOp::Pow:
    case JSOp::Pos:
    case JSOp::ToNumeric:
    case JSOp::Neg:
    case JSOp::Inc:
    case JSOp::Dec:
    case JSOp::ToString:
    case JSOp::Eq:
    case JSOp::Ne:
    case JSOp::StrictEq:
    case JSOp::StrictNe:
    case JSOp::StrictConstantEq:
    case JSOp::StrictConstantNe:
    case JSOp::Lt:
    case JSOp::Le:
    case JSOp::Gt:
    case JSOp::Ge:
    case JSOp::Double:
    case JSOp::BigInt:
    case JSOp::String:
    case JSOp::Symbol:
    case JSOp::Zero:
    case JSOp::One:
    case JSOp::Null:
    case JSOp::Void:
    case JSOp::Hole:
    case JSOp::False:
    case JSOp::True:
    case JSOp::Arguments:
    case JSOp::Rest:
    case JSOp::GetArg:
    case JSOp::GetFrameArg:
    case JSOp::SetArg:
    case JSOp::GetLocal:
    case JSOp::SetLocal:
    case JSOp::GetActualArg:
    case JSOp::ArgumentsLength:
    case JSOp::ThrowSetConst:
    case JSOp::CheckLexical:
    case JSOp::CheckAliasedLexical:
    case JSOp::InitLexical:
    case JSOp::Uninitialized:
    case JSOp::Pop:
    case JSOp::PopN:
    case JSOp::DupAt:
    case JSOp::NewArray:
    case JSOp::NewInit:
    case JSOp::NewObject:
    case JSOp::InitElem:
    case JSOp::InitHiddenElem:
    case JSOp::InitLockedElem:
    case JSOp::InitElemInc:
    case JSOp::InitElemArray:
    case JSOp::InitProp:
    case JSOp::InitLockedProp:
    case JSOp::InitHiddenProp:
    case JSOp::InitPropGetter:
    case JSOp::InitHiddenPropGetter:
    case JSOp::InitPropSetter:
    case JSOp::InitHiddenPropSetter:
    case JSOp::InitElemGetter:
    case JSOp::InitHiddenElemGetter:
    case JSOp::InitElemSetter:
    case JSOp::InitHiddenElemSetter:
    case JSOp::SpreadCall:
    case JSOp::Call:
    case JSOp::CallContent:
    case JSOp::CallIgnoresRv:
    case JSOp::CallIter:
    case JSOp::CallContentIter:
    case JSOp::New:
    case JSOp::NewContent:
    case JSOp::Eval:
    case JSOp::StrictEval:
    case JSOp::Int8:
    case JSOp::Uint16:
    case JSOp::ResumeKind:
    case JSOp::GetGName:
    case JSOp::GetName:
    case JSOp::GetIntrinsic:
    case JSOp::GetImport:
    case JSOp::BindName:
    case JSOp::BindUnqualifiedName:
    case JSOp::BindUnqualifiedGName:
    case JSOp::BindVar:
    case JSOp::Dup:
    case JSOp::Dup2:
    case JSOp::Swap:
    case JSOp::Pick:
    case JSOp::Unpick:
    case JSOp::GetAliasedDebugVar:
    case JSOp::GetAliasedVar:
    case JSOp::Uint24:
    case JSOp::Int32:
    case JSOp::LoopHead:
    case JSOp::GetElem:
    case JSOp::Not:
    case JSOp::FunctionThis:
    case JSOp::GlobalThis:
    case JSOp::NonSyntacticGlobalThis:
    case JSOp::Callee:
    case JSOp::EnvCallee:
    case JSOp::SuperBase:
    case JSOp::GetPropSuper:
    case JSOp::GetElemSuper:
    case JSOp::GetProp:
    case JSOp::RegExp:
    case JSOp::CallSiteObj:
    case JSOp::Object:
    case JSOp::Typeof:
    case JSOp::TypeofExpr:
    case JSOp::TypeofEq:
    case JSOp::ToAsyncIter:
    case JSOp::ToPropertyKey:
    case JSOp::Lambda:
    case JSOp::PushLexicalEnv:
    case JSOp::PopLexicalEnv:
    case JSOp::FreshenLexicalEnv:
    case JSOp::RecreateLexicalEnv:
    case JSOp::PushClassBodyEnv:
    case JSOp::Iter:
    case JSOp::MoreIter:
    case JSOp::IsNoIter:
    case JSOp::EndIter:
    case JSOp::CloseIter:
    case JSOp::OptimizeGetIterator:
    case JSOp::IsNullOrUndefined:
    case JSOp::In:
    case JSOp::HasOwn:
    case JSOp::CheckPrivateField:
    case JSOp::NewPrivateName:
    case JSOp::SetRval:
    case JSOp::Instanceof:
    case JSOp::DebugLeaveLexicalEnv:
    case JSOp::Debugger:
    case JSOp::ImplicitThis:
    case JSOp::NewTarget:
    case JSOp::CheckIsObj:
    case JSOp::CheckObjCoercible:
    case JSOp::DebugCheckSelfHosted:
    case JSOp::IsConstructing:
    case JSOp::OptimizeSpreadCall:
    case JSOp::ImportMeta:
    case JSOp::EnterWith:
    case JSOp::LeaveWith:
    case JSOp::SpreadNew:
    case JSOp::SpreadEval:
    case JSOp::StrictSpreadEval:
    case JSOp::CheckClassHeritage:
    case JSOp::FunWithProto:
    case JSOp::ObjWithProto:
    case JSOp::BuiltinObject:
    case JSOp::CheckThis:
    case JSOp::CheckReturn:
    case JSOp::CheckThisReinit:
    case JSOp::SuperFun:
    case JSOp::SpreadSuperCall:
    case JSOp::SuperCall:
    case JSOp::PushVarEnv:
    case JSOp::GetBoundName:
    case JSOp::Exception:
    case JSOp::ExceptionAndStack:
    case JSOp::IsGenClosing:
    case JSOp::FinalYieldRval:
    case JSOp::Resume:
    case JSOp::CheckResumeKind:
    case JSOp::AfterYield:
    case JSOp::MaybeExtractAwaitValue:
    case JSOp::Generator:
    case JSOp::AsyncAwait:
    case JSOp::AsyncResolve:
    case JSOp::AsyncReject:
    case JSOp::Finally:
    case JSOp::GetRval:
    case JSOp::ThrowMsg:
    case JSOp::ForceInterpreter:
      return false;

    case JSOp::InitAliasedLexical: {
      uint32_t hops = EnvironmentCoordinate(pc).hops();
      if (hops == 0) {
        // Initializing aliased lexical in the current scope is almost same
        // as JSOp::InitLexical.
        return false;
      }

      // Otherwise this can touch an environment outside of the current scope.
      return true;
    }
  }

  MOZ_ASSERT_UNREACHABLE("Invalid opcode");
  return false;
}

bool DebuggerScript::CallData::getEffectfulOffsets() {
  if (!ensureScript()) {
    return false;
  }

  RootedObject result(cx, NewDenseEmptyArray(cx));
  if (!result) {
    return false;
  }
  for (BytecodeRange r(cx, script); !r.empty(); r.popFront()) {
    size_t offset = r.frontOffset();
    if (!BytecodeIsEffectful(script, offset)) {
      continue;
    }

    if (IsGeneratorSlotInitialization(script, offset, cx)) {
      // This is engine-internal operation and not visible outside the
      // currently executing frame.
      //
      // Also this offset is not allowed for setting breakpoint.
      continue;
    }

    if (!NewbornArrayPush(cx, result, NumberValue(offset))) {
      return false;
    }
  }

  args.rval().setObject(*result);
  return true;
}

bool DebuggerScript::CallData::getAllOffsets() {
  if (!ensureScript()) {
    return false;
  }

  // First pass: determine which offsets in this script are jump targets and
  // which line numbers jump to them.
  FlowGraphSummary flowData(cx);
  if (!flowData.populate(cx, script)) {
    return false;
  }

  // Second pass: build the result array.
  RootedObject result(cx, NewDenseEmptyArray(cx));
  if (!result) {
    return false;
  }
  for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
    if (!r.frontIsEntryPoint()) {
      continue;
    }

    size_t offset = r.frontOffset();
    uint32_t lineno = r.frontLineNumber();

    // Make a note, if the current instruction is an entry point for the current
    // line.
    if (!flowData[offset].hasNoEdges() && flowData[offset].lineno() != lineno) {
      // Get the offsets array for this line.
      RootedObject offsets(cx);
      RootedValue offsetsv(cx);

      RootedId id(cx, PropertyKey::Int(lineno));

      bool found;
      if (!HasOwnProperty(cx, result, id, &found)) {
        return false;
      }
      if (found && !GetProperty(cx, result, result, id, &offsetsv)) {
        return false;
      }

      if (offsetsv.isObject()) {
        offsets = &offsetsv.toObject();
      } else {
        MOZ_ASSERT(offsetsv.isUndefined());

        // Create an empty offsets array for this line.
        // Store it in the result array.
        RootedId id(cx);
        RootedValue v(cx, NumberValue(lineno));
        offsets = NewDenseEmptyArray(cx);
        if (!offsets || !PrimitiveValueToId<CanGC>(cx, v, &id)) {
          return false;
        }

        RootedValue value(cx, ObjectValue(*offsets));
        if (!DefineDataProperty(cx, result, id, value)) {
          return false;
        }
      }

      // Append the current offset to the offsets array.
      if (!NewbornArrayPush(cx, offsets, NumberValue(offset))) {
        return false;
      }
    }
  }

  args.rval().setObject(*result);
  return true;
}

class DebuggerScript::GetAllColumnOffsetsMatcher {
  JSContext* cx_;
  MutableHandleObject result_;

  bool appendColumnOffsetEntry(uint32_t lineno,
                               JS::LimitedColumnNumberOneOrigin column,
                               size_t offset) {
    Rooted<PlainObject*> entry(cx_, NewPlainObject(cx_));
    if (!entry) {
      return false;
    }

    RootedValue value(cx_, NumberValue(lineno));
    if (!DefineDataProperty(cx_, entry, cx_->names().lineNumber, value)) {
      return false;
    }

    value = NumberValue(column.oneOriginValue());
    if (!DefineDataProperty(cx_, entry, cx_->names().columnNumber, value)) {
      return false;
    }

    value = NumberValue(offset);
    if (!DefineDataProperty(cx_, entry, cx_->names().offset, value)) {
      return false;
    }

    return NewbornArrayPush(cx_, result_, ObjectValue(*entry));
  }

 public:
  explicit GetAllColumnOffsetsMatcher(JSContext* cx, MutableHandleObject result)
      : cx_(cx), result_(result) {}
  using ReturnType = bool;
  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    // First pass: determine which offsets in this script are jump targets
    // and which positions jump to them.
    FlowGraphSummary flowData(cx_);
    if (!flowData.populate(cx_, script)) {
      return false;
    }

    // Second pass: build the result array.
    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) {
      uint32_t lineno = r.frontLineNumber();
      JS::LimitedColumnNumberOneOrigin column = r.frontColumnNumber();
      size_t offset = r.frontOffset();

      // Make a note, if the current instruction is an entry point for
      // the current position.
      if (r.frontIsEntryPoint() && !flowData[offset].hasNoEdges() &&
          (flowData[offset].lineno() != lineno ||
           flowData[offset].columnOrSentinel() != column.oneOriginValue())) {
        if (!appendColumnOffsetEntry(lineno, column, offset)) {
          return false;
        }
      }
    }
    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();

    Vector<wasm::ExprLoc> offsets(cx_);
    if (instance.debugEnabled() &&
        !instance.debug().getAllColumnOffsets(&offsets)) {
      return false;
    }

    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    for (uint32_t i = 0; i < offsets.length(); i++) {
      uint32_t lineno = offsets[i].lineno;
      JS::LimitedColumnNumberOneOrigin column(offsets[i].column);
      size_t offset = offsets[i].offset;
      if (!appendColumnOffsetEntry(lineno, column, offset)) {
        return false;
      }
    }
    return true;
  }
};

bool DebuggerScript::CallData::getAllColumnOffsets() {
  RootedObject result(cx);
  GetAllColumnOffsetsMatcher matcher(cx, &result);
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

class DebuggerScript::GetLineOffsetsMatcher {
  JSContext* cx_;
  uint32_t lineno_;
  MutableHandleObject result_;

 public:
  explicit GetLineOffsetsMatcher(JSContext* cx, uint32_t lineno,
                                 MutableHandleObject result)
      : cx_(cx), lineno_(lineno), result_(result) {}
  using ReturnType = bool;
  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    // First pass: determine which offsets in this script are jump targets and
    // which line numbers jump to them.
    FlowGraphSummary flowData(cx_);
    if (!flowData.populate(cx_, script)) {
      return false;
    }

    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    // Second pass: build the result array.
    for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) {
      if (!r.frontIsEntryPoint()) {
        continue;
      }

      size_t offset = r.frontOffset();

      // If the op at offset is an entry point, append offset to result.
      if (r.frontLineNumber() == lineno_ && !flowData[offset].hasNoEdges() &&
          flowData[offset].lineno() != lineno_) {
        if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) {
          return false;
        }
      }
    }

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();

    Vector<uint32_t> offsets(cx_);
    if (instance.debugEnabled() &&
        !instance.debug().getLineOffsets(lineno_, &offsets)) {
      return false;
    }

    result_.set(NewDenseEmptyArray(cx_));
    if (!result_) {
      return false;
    }

    for (uint32_t i = 0; i < offsets.length(); i++) {
      if (!NewbornArrayPush(cx_, result_, NumberValue(offsets[i]))) {
        return false;
      }
    }
    return true;
  }
};

bool DebuggerScript::CallData::getLineOffsets() {
  if (!args.requireAtLeast(cx, "Debugger.Script.getLineOffsets", 1)) {
    return false;
  }

  // Parse lineno argument.
  RootedValue linenoValue(cx, args[0]);
  uint32_t lineno;
  if (!ToNumber(cx, &linenoValue)) {
    return false;
  }
  {
    double d = linenoValue.toNumber();
    lineno = uint32_t(d);
    if (lineno != d) {
      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_LINE);
      return false;
    }
  }

  RootedObject result(cx);
  GetLineOffsetsMatcher matcher(cx, lineno, &result);
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setObject(*result);
  return true;
}

struct DebuggerScript::SetBreakpointMatcher {
  JSContext* cx_;
  Debugger* dbg_;
  size_t offset_;
  RootedObject handler_;
  RootedObject debuggerObject_;

  bool wrapCrossCompartmentEdges() {
    if (!cx_->compartment()->wrap(cx_, &handler_) ||
        !cx_->compartment()->wrap(cx_, &debuggerObject_)) {
      return false;
    }

    // If the Debugger's compartment has killed incoming wrappers, we may not
    // have gotten usable results from the 'wrap' calls. Treat it as a
    // failure.
    if (IsDeadProxyObject(handler_) || IsDeadProxyObject(debuggerObject_)) {
      ReportAccessDenied(cx_);
      return false;
    }

    return true;
  }

 public:
  explicit SetBreakpointMatcher(JSContext* cx, Debugger* dbg, size_t offset,
                                HandleObject handler)
      : cx_(cx),
        dbg_(dbg),
        offset_(offset),
        handler_(cx, handler),
        debuggerObject_(cx_, dbg_->toJSObject()) {}

  using ReturnType = bool;

  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    if (!dbg_->observesScript(script)) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_NOT_DEBUGGING);
      return false;
    }

    if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
      return false;
    }

    if (!EnsureBreakpointIsAllowed(cx_, script, offset_)) {
      return false;
    }

    // Ensure observability *before* setting the breakpoint. If the script is
    // not already a debuggee, trying to ensure observability after setting
    // the breakpoint (and thus marking the script as a debuggee) will skip
    // actually ensuring observability.
    if (!dbg_->ensureExecutionObservabilityOfScript(cx_, script)) {
      return false;
    }

    // A Breakpoint belongs logically to its script's compartment, so its
    // references to its Debugger and handler must be properly wrapped.
    AutoRealm ar(cx_, script);
    if (!wrapCrossCompartmentEdges()) {
      return false;
    }

    jsbytecode* pc = script->offsetToPC(offset_);
    JSBreakpointSite* site =
        DebugScript::getOrCreateBreakpointSite(cx_, script, pc);
    if (!site) {
      return false;
    }

    if (!cx_->zone()->new_<Breakpoint>(dbg_, debuggerObject_, site, handler_)) {
      site->destroyIfEmpty(cx_->runtime()->gcContext());
      return false;
    }
    AddCellMemory(script, sizeof(Breakpoint), MemoryUse::Breakpoint);

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    wasm::Instance& instance = wasmInstance->instance();
    if (!instance.debugEnabled() ||
        !instance.debug().hasBreakpointTrapAtOffset(offset_)) {
      JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
                                JSMSG_DEBUG_BAD_OFFSET);
      return false;
    }

    // A Breakpoint belongs logically to its Instance's compartment, so its
    // references to its Debugger and handler must be properly wrapped.
    AutoRealm ar(cx_, wasmInstance);
    if (!wrapCrossCompartmentEdges()) {
      return false;
    }

    WasmBreakpointSite* site = instance.getOrCreateBreakpointSite(cx_, offset_);
    if (!site) {
      return false;
    }

    if (!cx_->zone()->new_<Breakpoint>(dbg_, debuggerObject_, site, handler_)) {
      site->destroyIfEmpty(cx_->runtime()->gcContext());
      return false;
    }
    AddCellMemory(wasmInstance, sizeof(Breakpoint), MemoryUse::Breakpoint);

    return true;
  }
};

bool DebuggerScript::CallData::setBreakpoint() {
  if (!args.requireAtLeast(cx, "Debugger.Script.setBreakpoint", 2)) {
    return false;
  }
  Debugger* dbg = obj->owner();

  size_t offset;
  if (!ScriptOffset(cx, args[0], &offset)) {
    return false;
  }

  RootedObject handler(cx, RequireObject(cx, args[1]));
  if (!handler) {
    return false;
  }

  SetBreakpointMatcher matcher(cx, dbg, offset, handler);
  if (!referent.match(matcher)) {
    return false;
  }
  args.rval().setUndefined();
  return true;
}

bool DebuggerScript::CallData::getBreakpoints() {
  if (!ensureScript()) {
    return false;
  }
  Debugger* dbg = obj->owner();

  jsbytecode* pc;
  if (args.length() > 0) {
    size_t offset;
    if (!ScriptOffset(cx, args[0], &offset) ||
        !EnsureScriptOffsetIsValid(cx, script, offset)) {
      return false;
    }
    pc = script->offsetToPC(offset);
  } else {
    pc = nullptr;
  }

  RootedObject arr(cx, NewDenseEmptyArray(cx));
  if (!arr) {
    return false;
  }

  for (unsigned i = 0; i < script->length(); i++) {
    JSBreakpointSite* site =
        DebugScript::getBreakpointSite(script, script->offsetToPC(i));
    if (!site) {
      continue;
    }
    if (!pc || site->pc == pc) {
      for (Breakpoint* bp = site->firstBreakpoint(); bp;
           bp = bp->nextInSite()) {
        if (bp->debugger == dbg) {
          RootedObject handler(cx, bp->getHandler());
          if (!cx->compartment()->wrap(cx, &handler) ||
              !NewbornArrayPush(cx, arr, ObjectValue(*handler))) {
            return false;
          }
        }
      }
    }
  }
  args.rval().setObject(*arr);
  return true;
}

class DebuggerScript::ClearBreakpointMatcher {
  JSContext* cx_;
  Debugger* dbg_;
  RootedObject handler_;

 public:
  ClearBreakpointMatcher(JSContext* cx, Debugger* dbg, JSObject* handler)
      : cx_(cx), dbg_(dbg), handler_(cx, handler) {}
  using ReturnType = bool;

  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    // A Breakpoint belongs logically to its script's compartment, so it holds
    // its handler via a cross-compartment wrapper. But the handler passed to
    // `clearBreakpoint` is same-compartment with the Debugger. Wrap it here,
    // so that `DebugScript::clearBreakpointsIn` gets the right value to
    // search for.
    AutoRealm ar(cx_, script);
    if (!cx_->compartment()->wrap(cx_, &handler_)) {
      return false;
    }

    DebugScript::clearBreakpointsIn(cx_->runtime()->gcContext(), script, dbg_,
                                    handler_);
    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    if (!instance.debugEnabled()) {
      return true;
    }

    // A Breakpoint belongs logically to its instance's compartment, so it
    // holds its handler via a cross-compartment wrapper. But the handler
    // passed to `clearBreakpoint` is same-compartment with the Debugger. Wrap
    // it here, so that `DebugState::clearBreakpointsIn` gets the right value
    // to search for.
    AutoRealm ar(cx_, instanceObj);
    if (!cx_->compartment()->wrap(cx_, &handler_)) {
      return false;
    }

    instance.debug().clearBreakpointsIn(cx_->runtime()->gcContext(),
                                        instanceObj, dbg_, handler_);
    return true;
  }
};

bool DebuggerScript::CallData::clearBreakpoint() {
  if (!args.requireAtLeast(cx, "Debugger.Script.clearBreakpoint", 1)) {
    return false;
  }
  Debugger* dbg = obj->owner();

  JSObject* handler = RequireObject(cx, args[0]);
  if (!handler) {
    return false;
  }

  ClearBreakpointMatcher matcher(cx, dbg, handler);
  if (!referent.match(matcher)) {
    return false;
  }

  args.rval().setUndefined();
  return true;
}

bool DebuggerScript::CallData::clearAllBreakpoints() {
  Debugger* dbg = obj->owner();
  ClearBreakpointMatcher matcher(cx, dbg, nullptr);
  if (!referent.match(matcher)) {
    return false;
  }
  args.rval().setUndefined();
  return true;
}

class DebuggerScript::IsInCatchScopeMatcher {
  JSContext* cx_;
  size_t offset_;
  bool isInCatch_;

 public:
  explicit IsInCatchScopeMatcher(JSContext* cx, size_t offset)
      : cx_(cx), offset_(offset), isInCatch_(false) {}
  using ReturnType = bool;

  inline bool isInCatch() const { return isInCatch_; }

  ReturnType match(Handle<BaseScript*> base) {
    RootedScript script(cx_, DelazifyScript(cx_, base));
    if (!script) {
      return false;
    }

    if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
      return false;
    }

    MOZ_ASSERT(!isInCatch_);
    for (const TryNote& tn : script->trynotes()) {
      bool inRange = tn.start <= offset_ && offset_ < tn.start + tn.length;
      if (inRange && tn.kind() == TryNoteKind::Catch) {
        isInCatch_ = true;
      } else if (isInCatch_) {
        // For-of loops generate a synthetic catch block to handle
        // closing the iterator when throwing an exception. The
        // debugger should ignore these synthetic catch blocks, so
        // we skip any Catch trynote that is immediately followed
        // by a ForOf trynote.
        if (inRange && tn.kind() == TryNoteKind::ForOf) {
          isInCatch_ = false;
          continue;
        }
        return true;
      }
    }

    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instance) {
    isInCatch_ = false;
    return true;
  }
};

bool DebuggerScript::CallData::isInCatchScope() {
  if (!args.requireAtLeast(cx, "Debugger.Script.isInCatchScope", 1)) {
    return false;
  }

  size_t offset;
  if (!ScriptOffset(cx, args[0], &offset)) {
    return false;
  }

  IsInCatchScopeMatcher matcher(cx, offset);
  if (!referent.match(matcher)) {
    return false;
  }
  args.rval().setBoolean(matcher.isInCatch());
  return true;
}

bool DebuggerScript::CallData::getOffsetsCoverage() {
  if (!ensureScript()) {
    return false;
  }

  Debugger* dbg = obj->owner();
  if (dbg->observesCoverage() != Debugger::Observing) {
    args.rval().setNull();
    return true;
  }

  // If the script has no coverage information, then skip this and return null
  // instead.
  if (!script->hasScriptCounts()) {
    args.rval().setNull();
    return true;
  }

  ScriptCounts* sc = &script->getScriptCounts();

  // If the main ever got visited, then assume that any code before main got
  // visited once.
  uint64_t hits = 0;
  const PCCounts* counts =
      sc->maybeGetPCCounts(script->pcToOffset(script->main()));
  if (counts->numExec()) {
    hits = 1;
  }

  // Build an array of objects which are composed of 4 properties:
  //  - offset          PC offset of the current opcode.
  //  - lineNumber      Line of the current opcode.
  //  - columnNumber    Column of the current opcode.
  //  - count           Number of times the instruction got executed.
  RootedObject result(cx, NewDenseEmptyArray(cx));
  if (!result) {
    return false;
  }

  RootedId offsetId(cx, NameToId(cx->names().offset));
  RootedId lineNumberId(cx, NameToId(cx->names().lineNumber));
  RootedId columnNumberId(cx, NameToId(cx->names().columnNumber));
  RootedId countId(cx, NameToId(cx->names().count));

  RootedObject item(cx);
  RootedValue offsetValue(cx);
  RootedValue lineNumberValue(cx);
  RootedValue columnNumberValue(cx);
  RootedValue countValue(cx);

  // Iterate linearly over the bytecode.
  for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
    size_t offset = r.frontOffset();

    // The beginning of each non-branching sequences of instruction set the
    // number of execution of the current instruction and any following
    // instruction.
    counts = sc->maybeGetPCCounts(offset);
    if (counts) {
      hits = counts->numExec();
    }

    offsetValue.setNumber(double(offset));
    lineNumberValue.setNumber(double(r.frontLineNumber()));
    columnNumberValue.setNumber(double(r.frontColumnNumber().oneOriginValue()));
    countValue.setNumber(double(hits));

    // Create a new object with the offset, line number, column number, the
    // number of hit counts, and append it to the array.
    item = NewPlainObjectWithProto(cx, nullptr);
    if (!item || !DefineDataProperty(cx, item, offsetId, offsetValue) ||
        !DefineDataProperty(cx, item, lineNumberId, lineNumberValue) ||
        !DefineDataProperty(cx, item, columnNumberId, columnNumberValue) ||
        !DefineDataProperty(cx, item, countId, countValue) ||
        !NewbornArrayPush(cx, result, ObjectValue(*item))) {
      return false;
    }

    // If the current instruction has thrown, then decrement the hit counts
    // with the number of throws.
    counts = sc->maybeGetThrowCounts(offset);
    if (counts) {
      hits -= counts->numExec();
    }
  }

  args.rval().setObject(*result);
  return true;
}

/* static */
bool DebuggerScript::construct(JSContext* cx, unsigned argc, Value* vp) {
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                            "Debugger.Script");
  return false;
}

const JSPropertySpec DebuggerScript::properties_[] = {
    JS_DEBUG_PSG("isGeneratorFunction", getIsGeneratorFunction),
    JS_DEBUG_PSG("isAsyncFunction", getIsAsyncFunction),
    JS_DEBUG_PSG("isFunction", getIsFunction),
    JS_DEBUG_PSG("isModule", getIsModule),
    JS_DEBUG_PSG("displayName", getDisplayName),
    JS_DEBUG_PSG("parameterNames", getParameterNames),
    JS_DEBUG_PSG("url", getUrl),
    JS_DEBUG_PSG("startLine", getStartLine),
    JS_DEBUG_PSG("startColumn", getStartColumn),
    JS_DEBUG_PSG("lineCount", getLineCount),
    JS_DEBUG_PSG("source", getSource),
    JS_DEBUG_PSG("sourceStart", getSourceStart),
    JS_DEBUG_PSG("sourceLength", getSourceLength),
    JS_DEBUG_PSG("mainOffset", getMainOffset),
    JS_DEBUG_PSG("global", getGlobal),
    JS_DEBUG_PSG("format", getFormat),
    JS_PS_END,
};

const JSFunctionSpec DebuggerScript::methods_[] = {
    JS_DEBUG_FN("getChildScripts", getChildScripts, 0),
    JS_DEBUG_FN("getPossibleBreakpoints", getPossibleBreakpoints, 0),
    JS_DEBUG_FN("getPossibleBreakpointOffsets", getPossibleBreakpointOffsets,
                0),
    JS_DEBUG_FN("setBreakpoint", setBreakpoint, 2),
    JS_DEBUG_FN("getBreakpoints", getBreakpoints, 1),
    JS_DEBUG_FN("clearBreakpoint", clearBreakpoint, 1),
    JS_DEBUG_FN("clearAllBreakpoints", clearAllBreakpoints, 0),
    JS_DEBUG_FN("isInCatchScope", isInCatchScope, 1),
    JS_DEBUG_FN("getOffsetMetadata", getOffsetMetadata, 1),
    JS_DEBUG_FN("getOffsetsCoverage", getOffsetsCoverage, 0),
    JS_DEBUG_FN("getEffectfulOffsets", getEffectfulOffsets, 1),

    // The following APIs are deprecated due to their reliance on the
    // under-defined 'entrypoint' concept. Make use of getPossibleBreakpoints,
    // getPossibleBreakpointOffsets, or getOffsetMetadata instead.
    JS_DEBUG_FN("getAllOffsets", getAllOffsets, 0),
    JS_DEBUG_FN("getAllColumnOffsets", getAllColumnOffsets, 0),
    JS_DEBUG_FN("getLineOffsets", getLineOffsets, 1),
    JS_DEBUG_FN("getOffsetLocation", getOffsetLocation, 0),
    JS_FS_END,
};
