Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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;
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");
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");
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::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