author | Logan Smyth <loganfsmyth@gmail.com> |
Wed, 13 Feb 2019 02:31:00 +0000 | |
changeset 458905 | 5c934ede1cfc |
parent 458904 | 5add2761a3b6 |
child 458906 | 62f3c188b868 |
push id | 35551 |
push user | shindli@mozilla.com |
push date | Wed, 13 Feb 2019 21:34:09 +0000 |
treeherder | mozilla-central@08f794a4928e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | jimb, bhackett |
bugs | 1518661 |
milestone | 67.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/js/src/doc/Debugger/Debugger.Script.md +++ b/js/src/doc/Debugger/Debugger.Script.md @@ -200,16 +200,164 @@ from its prototype: **If the instance refers to WebAssembly code**, `"wasm"`. ## Function Properties of the Debugger.Script Prototype Object The functions described below may only be called with a `this` value referring to a `Debugger.Script` instance; they may not be used as methods of other kinds of objects. +`getChildScripts()` +: **If the instance refers to a `JSScript`**, return a new array whose + elements are Debugger.Script objects for each function + in this script. Only direct children are included; nested + children can be reached by walking the tree. + + **If the instance refers to WebAssembly code**, throw a `TypeError`. + +<code>getPossibleBreakpoints(<i>query</i>)</code> +: Query for the recommended breakpoint locations available in SpiderMonkey. + Returns a result array of objects with the following properties: + * `offset: number` - The offset the breakpoint. + * `lineNumber: number` - The line number of the breakpoint. + * `columnNumber: number` - The column number of the breakpoint. + * `isStepStart: boolean` - True if SpiderMonkey recommends that the + breakpoint be treated as a step location when users of debuggers + step to the next item. This _roughly_ translates to the start of + each statement, though not entirely. + + The `query` argument can be used to filter the set of breakpoints. + The `query` object can contain the following properties: + + * `minOffset: number` - The inclusive lower bound of `offset` values to include. + * `maxOffset: number` - The exclusive upper bound of `offset` values to include. + * `line: number` - Limit to breakpoints on the given line. + * `minLine: number` - The inclusive lower bound of lines to include. + * `minColumn: number` - The inclusive lower bound of the line/minLine column to include. + * `maxLine: number` - The exclusive upper bound of lines to include. + * `maxColumn: number` - The exclusive upper bound of the line/maxLine column to include. + +<code>getPossibleBreakpointOffsets(<i>query</i>)</code> +: Query for the recommended breakpoint locations available in SpiderMonkey. + Identical to getPossibleBreakpoints except this returns an array of `offset` + values instead of offset metadata objects. + +<code>getOffsetMetadata(<i>offset</i>)</code> +: Get metadata about a given bytecode offset. + Returns an object with the following properties: + * `lineNumber: number` - The line number of the breakpoint. + * `columnNumber: number` - The column number of the breakpoint. + * `isBreakpoint: boolean` - True if this offset qualifies as a breakpoint, + defined using the same semantics used for `getPossibleBreakpoints()`. + * `isStepStart: boolean` - True if SpiderMonkey recommends that the + breakpoint be treated as a step location when users of debuggers + step to the next item. This _roughly_ translates to the start of + each statement, though not entirely. + +<code>setBreakpoint(<i>offset</i>, <i>handler</i>)</code> +: **If the instance refers to a `JSScript`**, set a breakpoint at the + bytecode instruction at <i>offset</i> in this script, reporting hits to + the `hit` method of <i>handler</i>. If <i>offset</i> is not a valid offset + in this script, throw an error. + + When execution reaches the given instruction, SpiderMonkey calls the + `hit` method of <i>handler</i>, passing a [`Debugger.Frame`][frame] + instance representing the currently executing stack frame. The `hit` + method's return value should be a [resumption value][rv], determining + how execution should continue. + + Any number of breakpoints may be set at a single location; when control + reaches that point, SpiderMonkey calls their handlers in an unspecified + order. + + Any number of breakpoints may use the same <i>handler</i> object. + + Breakpoint handler method calls are cross-compartment, intra-thread + calls: the call takes place in the same thread that hit the breakpoint, + and in the compartment containing the handler function (typically the + debugger's compartment). + + The new breakpoint belongs to the [`Debugger`][debugger-object] instance to + which this script belongs. Disabling the [`Debugger`][debugger-object] + instance disables this breakpoint; and removing a global from the + [`Debugger`][debugger-object] instance's set of debuggees clears all the + breakpoints belonging to that [`Debugger`][debugger-object] instance in that + global's scripts. + +<code>getBreakpoints([<i>offset</i>])</code> +: **If the instance refers to a `JSScript`**, return an array containing the + handler objects for all the breakpoints set at <i>offset</i> in this + script. If <i>offset</i> is omitted, return the handlers of all + breakpoints set anywhere in this script. If <i>offset</i> is present, but + not a valid offset in this script, throw an error. + + **If the instance refers to WebAssembly code**, throw a `TypeError`. + +<code>clearBreakpoint(handler, [<i>offset</i>])</code> +: **If the instance refers to a `JSScript`**, remove all breakpoints set in + this [`Debugger`][debugger-object] instance that use <i>handler</i> as + their handler. If <i>offset</i> is given, remove only those breakpoints + set at <i>offset</i> that use <i>handler</i>; if <i>offset</i> is not a + valid offset in this script, throw an error. + + Note that, if breakpoints using other handler objects are set at the + same location(s) as <i>handler</i>, they remain in place. + +<code>clearAllBreakpoints([<i>offset</i>])</code> +: **If the instance refers to a `JSScript`**, remove all breakpoints set in + this script. If <i>offset</i> is present, remove all breakpoints set at + that offset in this script; if <i>offset</i> is not a valid bytecode + offset in this script, throw an error. + +<code>getSuccessorOffsets(<i>offset</i>)</code> +: **If the instance refers to a `JSScript`**, return an array + containing the offsets of all bytecodes in the script which are + immediate successors of <i>offset</i> via non-exceptional control + flow paths. + +<code>getPredecessorOffsets(<i>offset</i>)</code> +: **If the instance refers to a `JSScript`**, return an array + containing the offsets of all bytecodes in the script for which + <i>offset</i> is an immediate successor via non-exceptional + control flow paths. + +`getOffsetsCoverage()`: +: **If the instance refers to a `JSScript`**, return `null` or an array which + contains information about the coverage of all opcodes. The elements of + the array are objects, each of which describes a single opcode, and + contains the following properties: + + * lineNumber: the line number of the current opcode. + + * columnNumber: the column number of the current opcode. + + * offset: the bytecode instruction offset of the current opcode. + + * count: the number of times the current opcode got executed. + + If this script has no coverage, or if it is not instrumented, then this + function will return `null`. To ensure that the debuggee is instrumented, + the flag `Debugger.collectCoverageInfo` should be set to `true`. + + **If the instance refers to WebAssembly code**, throw a `TypeError`. + +<code>isInCatchScope([<i>offset</i>])</code> +: **If the instance refers to a `JSScript`**, this is `true` if this offset + falls within the scope of a try block, and `false` otherwise. + + **If the instance refers to WebAssembly code**, throw a `TypeError`. + + +### Deprecated Debugger.Script Prototype Functions + +The following functions have all been deprecated in favor of `getOffsetMetadata`, +`getPossibleBreakpoints`, and `getPossibleBreakpointOffsets`. These functions +all have an under-defined concept of what offsets are and are not included +in their results. + `getAllOffsets()` : **If the instance refers to a `JSScript`**, return an array <i>L</i> describing the relationship between bytecode instruction offsets and source code positions in this script. <i>L</i> is sparse, and indexed by source line number. If a source line number <i>line</i> has no code, then <i>L</i> has no <i>line</i> property. If there is code for <i>line</i>, then <code><i>L</i>[<i>line</i>]</code> is an array of offsets of byte code instructions that are entry points to that line. @@ -291,109 +439,8 @@ methods of other kinds of objects. script. The object has the following properties: * lineNumber: the line number for which offset is an entry point * columnNumber: the column number for which offset is an entry point * isEntryPoint: true if the offset is a column entry point, as would be reported by getAllColumnOffsets(); otherwise false. - -<code>getSuccessorOffsets(<i>offset</i>)</code> -: **If the instance refers to a `JSScript`**, return an array - containing the offsets of all bytecodes in the script which are - immediate successors of <i>offset</i> via non-exceptional control - flow paths. - -<code>getPredecessorOffsets(<i>offset</i>)</code> -: **If the instance refers to a `JSScript`**, return an array - containing the offsets of all bytecodes in the script for which - <i>offset</i> is an immediate successor via non-exceptional - control flow paths. - -`getOffsetsCoverage()`: -: **If the instance refers to a `JSScript`**, return `null` or an array which - contains information about the coverage of all opcodes. The elements of - the array are objects, each of which describes a single opcode, and - contains the following properties: - - * lineNumber: the line number of the current opcode. - - * columnNumber: the column number of the current opcode. - - * offset: the bytecode instruction offset of the current opcode. - - * count: the number of times the current opcode got executed. - - If this script has no coverage, or if it is not instrumented, then this - function will return `null`. To ensure that the debuggee is instrumented, - the flag `Debugger.collectCoverageInfo` should be set to `true`. - - **If the instance refers to WebAssembly code**, throw a `TypeError`. - -`getChildScripts()` -: **If the instance refers to a `JSScript`**, return a new array whose - elements are Debugger.Script objects for each function - in this script. Only direct children are included; nested - children can be reached by walking the tree. - - **If the instance refers to WebAssembly code**, throw a `TypeError`. - -<code>setBreakpoint(<i>offset</i>, <i>handler</i>)</code> -: **If the instance refers to a `JSScript`**, set a breakpoint at the - bytecode instruction at <i>offset</i> in this script, reporting hits to - the `hit` method of <i>handler</i>. If <i>offset</i> is not a valid offset - in this script, throw an error. - - When execution reaches the given instruction, SpiderMonkey calls the - `hit` method of <i>handler</i>, passing a [`Debugger.Frame`][frame] - instance representing the currently executing stack frame. The `hit` - method's return value should be a [resumption value][rv], determining - how execution should continue. - - Any number of breakpoints may be set at a single location; when control - reaches that point, SpiderMonkey calls their handlers in an unspecified - order. - - Any number of breakpoints may use the same <i>handler</i> object. - - Breakpoint handler method calls are cross-compartment, intra-thread - calls: the call takes place in the same thread that hit the breakpoint, - and in the compartment containing the handler function (typically the - debugger's compartment). - - The new breakpoint belongs to the [`Debugger`][debugger-object] instance to - which this script belongs. Disabling the [`Debugger`][debugger-object] - instance disables this breakpoint; and removing a global from the - [`Debugger`][debugger-object] instance's set of debuggees clears all the - breakpoints belonging to that [`Debugger`][debugger-object] instance in that - global's scripts. - -<code>getBreakpoints([<i>offset</i>])</code> -: **If the instance refers to a `JSScript`**, return an array containing the - handler objects for all the breakpoints set at <i>offset</i> in this - script. If <i>offset</i> is omitted, return the handlers of all - breakpoints set anywhere in this script. If <i>offset</i> is present, but - not a valid offset in this script, throw an error. - - **If the instance refers to WebAssembly code**, throw a `TypeError`. - -<code>clearBreakpoint(handler, [<i>offset</i>])</code> -: **If the instance refers to a `JSScript`**, remove all breakpoints set in - this [`Debugger`][debugger-object] instance that use <i>handler</i> as - their handler. If <i>offset</i> is given, remove only those breakpoints - set at <i>offset</i> that use <i>handler</i>; if <i>offset</i> is not a - valid offset in this script, throw an error. - - Note that, if breakpoints using other handler objects are set at the - same location(s) as <i>handler</i>, they remain in place. - -<code>clearAllBreakpoints([<i>offset</i>])</code> -: **If the instance refers to a `JSScript`**, remove all breakpoints set in - this script. If <i>offset</i> is present, remove all breakpoints set at - that offset in this script; if <i>offset</i> is not a valid bytecode - offset in this script, throw an error. - -<code>isInCatchScope([<i>offset</i>])</code> -: **If the instance refers to a `JSScript`**, this is `true` if this offset - falls within the scope of a try block, and `false` otherwise. - - **If the instance refers to WebAssembly code**, throw a `TypeError`.
--- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -97,16 +97,19 @@ BytecodeEmitter::BytecodeEmitter(Bytecod parent(parent), script(cx, script), lazyScript(cx, lazyScript), code_(cx), notes_(cx), lastNoteOffset_(0), currentLine_(lineNum), lastColumn_(0), + lastSeparatorOffet_(0), + lastSeparatorLine_(0), + lastSeparatorColumn_(0), mainOffset_(), lastTarget{-1 - ptrdiff_t(JSOP_JUMPTARGET_LENGTH)}, parser(nullptr), atomIndices(cx->frontendCollectionPool()), firstLine(lineNum), maxFixedSlots(0), maxStackDepth(0), stackDepth(0), @@ -189,16 +192,60 @@ Maybe<NameLocation> BytecodeEmitter::loc JSAtom* name, EmitterScope* source) { EmitterScope* funScope = source; while (!funScope->scope(this)->is<FunctionScope>()) { funScope = funScope->enclosingInFrame(); } return source->locationBoundInScope(name, funScope); } +bool BytecodeEmitter::markStepBreakpoint() { + if (inPrologue()) { + return true; + } + + if (!newSrcNote(SRC_STEP_SEP)) { + return false; + } + + if (!newSrcNote(SRC_BREAKPOINT)) { + return false; + } + + // We track the location of the most recent separator for use in + // markSimpleBreakpoint. Note that this means that the position must already + // be set before markStepBreakpoint is called. + lastSeparatorOffet_ = code().length(); + lastSeparatorLine_ = currentLine_; + lastSeparatorColumn_ = lastColumn_; + + return true; +} + +bool BytecodeEmitter::markSimpleBreakpoint() { + if (inPrologue()) { + return true; + } + + // If a breakable call ends up being the same location as the most recent + // expression start, we need to skip marking it breakable in order to avoid + // having two breakpoints with the same line/column position. + // Note: This assumes that the position for the call has already been set. + bool isDuplicateLocation = + lastSeparatorLine_ == currentLine_ && lastSeparatorColumn_ == lastColumn_; + + if (!isDuplicateLocation) { + if (!newSrcNote(SRC_BREAKPOINT)) { + return false; + } + } + + return true; +} + bool BytecodeEmitter::emitCheck(JSOp op, ptrdiff_t delta, ptrdiff_t* offset) { *offset = code().length(); if (!code().growBy(delta)) { ReportOutOfMemory(cx); return false; } @@ -515,16 +562,18 @@ bool BytecodeEmitter::updateLineNumberNo } } else { do { if (!newSrcNote(SRC_NEWLINE)) { return false; } } while (--delta != 0); } + + updateSeparatorPosition(); } return true; } /* Updates the line number and column number information in the source notes. */ bool BytecodeEmitter::updateSourceCoordNotes(uint32_t offset) { if (!updateLineNumberNotes(offset)) { return false; @@ -545,18 +594,27 @@ bool BytecodeEmitter::updateSourceCoordN // but it's better to fail soft here. if (!SN_REPRESENTABLE_COLSPAN(colspan)) { return true; } if (!newSrcNote2(SRC_COLSPAN, SN_COLSPAN_TO_OFFSET(colspan))) { return false; } lastColumn_ = columnIndex; - } - return true; + updateSeparatorPosition(); + } + return true; +} + +/* Updates the last separator position, if present */ +void BytecodeEmitter::updateSeparatorPosition() { + if (!inPrologue() && lastSeparatorOffet_ == code().length()) { + lastSeparatorLine_ = currentLine_; + lastSeparatorColumn_ = lastColumn_; + } } Maybe<uint32_t> BytecodeEmitter::getOffsetForLoop(ParseNode* nextpn) { if (!nextpn) { return Nothing(); } // Try to give the JSOP_LOOPHEAD and JSOP_LOOPENTRY the same line number as @@ -2007,16 +2065,20 @@ MOZ_NEVER_INLINE bool BytecodeEmitter::e MOZ_ASSERT(lexical.isKind(ParseNodeKind::LexicalScope)); ListNode* cases = &lexical.scopeBody()->as<ListNode>(); MOZ_ASSERT(cases->isKind(ParseNodeKind::StatementList)); SwitchEmitter se(this); if (!se.emitDiscriminant(Some(switchStmt->discriminant().pn_pos.begin))) { return false; } + + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(&switchStmt->discriminant())) { return false; } // Enter the scope before pushing the switch BreakableControl since all // breaks are under this scope. if (!lexical.isEmptyScope()) { @@ -2387,16 +2449,19 @@ bool BytecodeEmitter::emitScript(ParseNo if (!emitTree(body)) { return false; } if (!updateSourceCoordNotes(body->pn_pos.end)) { return false; } } + if (!markSimpleBreakpoint()) { + return false; + } if (!emit1(JSOP_RETRVAL)) { return false; } if (!emitterScope.leave(this)) { return false; } @@ -2449,16 +2514,19 @@ bool BytecodeEmitter::emitFunctionScript setFunctionBodyEndPos(body->pn_pos); if (!emitTree(body)) { return false; } if (!updateSourceCoordNotes(body->pn_pos.end)) { return false; } + if (!markSimpleBreakpoint()) { + return false; + } // Always end the script with a JSOP_RETRVAL. Some other parts of the // codebase depend on this opcode, // e.g. InterpreterRegs::setToEndOfScript. if (!emit1(JSOP_RETRVAL)) { return false; } @@ -3936,16 +4004,19 @@ bool BytecodeEmitter::emitDeclarationLis AssignmentNode* assignNode = &decl->as<AssignmentNode>(); ListNode* pattern = &assignNode->left()->as<ListNode>(); MOZ_ASSERT(pattern->isKind(ParseNodeKind::ArrayExpr) || pattern->isKind(ParseNodeKind::ObjectExpr)); if (!updateSourceCoordNotes(assignNode->right()->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(assignNode->right())) { return false; } if (!emitDestructuringOps(pattern, DestructuringDeclaration)) { return false; } @@ -3987,16 +4058,19 @@ bool BytecodeEmitter::emitSingleDeclarat return false; } } else { MOZ_ASSERT(initializer); if (!updateSourceCoordNotes(initializer->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitInitializer(initializer, decl)) { // [stack] ENV? V return false; } } if (!noe.emitAssignment()) { // [stack] V return false; @@ -4646,16 +4720,20 @@ MOZ_MUST_USE bool BytecodeEmitter::emitG bool BytecodeEmitter::emitIf(TernaryNode* ifNode) { IfEmitter ifThenElse(this); if (!ifThenElse.emitIf(Some(ifNode->kid1()->pn_pos.begin))) { return false; } if_again: + if (!markStepBreakpoint()) { + return false; + } + /* Emit code for the condition before pushing stmtInfo. */ if (!emitTree(ifNode->kid1())) { return false; } ParseNode* elseNode = ifNode->kid3(); if (elseNode) { if (!ifThenElse.emitThenElse()) { @@ -4814,16 +4892,20 @@ MOZ_NEVER_INLINE bool BytecodeEmitter::e } bool BytecodeEmitter::emitWith(BinaryNode* withNode) { // Ensure that the column of the 'with' is set properly. if (!updateSourceCoordNotes(withNode->left()->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } + if (!emitTree(withNode->left())) { return false; } EmitterScope emitterScope(this); if (!emitterScope.enterWith(this)) { return false; } @@ -5271,16 +5353,19 @@ bool BytecodeEmitter::emitForOf(ForNode* if (!forOf.emitIterated()) { // [stack] return false; } if (!updateSourceCoordNotes(forHeadExpr->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(forHeadExpr)) { // [stack] ITERABLE return false; } if (headLexicalEmitterScope) { DebugOnly<ParseNode*> forOfTarget = forOfHead->kid1(); MOZ_ASSERT(forOfTarget->isKind(ParseNodeKind::LetDecl) || @@ -5369,16 +5454,19 @@ bool BytecodeEmitter::emitForIn(ForNode* } // Evaluate the expression being iterated. ParseNode* expr = forInHead->kid3(); if (!updateSourceCoordNotes(expr->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(expr)) { // [stack] EXPR return false; } MOZ_ASSERT(forInLoop->iflags() == 0); MOZ_ASSERT_IF(headLexicalEmitterScope, @@ -5444,16 +5532,19 @@ bool BytecodeEmitter::emitCStyleFor( if (!emitTree(init)) { // [stack] return false; } } else { if (!updateSourceCoordNotes(init->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } // 'init' is an expression, not a declaration. emitTree left its // value on the stack. if (!emitTree(init, ValueUsage::IgnoreValue)) { // [stack] VAL return false; } if (!emit1(JSOP_POP)) { @@ -5482,16 +5573,19 @@ bool BytecodeEmitter::emitCStyleFor( return false; } // Check for update code to do before the condition (if any). if (update) { if (!updateSourceCoordNotes(update->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(update, ValueUsage::IgnoreValue)) { // [stack] VAL return false; } } if (!cfor.emitCond(Some(forNode->pn_pos.begin), cond ? Some(cond->pn_pos.begin) : Nothing(), @@ -5499,16 +5593,19 @@ bool BytecodeEmitter::emitCStyleFor( // [stack] return false; } if (cond) { if (!updateSourceCoordNotes(cond->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(cond)) { // [stack] VAL return false; } } if (!cfor.emitEnd()) { // [stack] @@ -5863,16 +5960,19 @@ bool BytecodeEmitter::emitDo(BinaryNode* if (!doWhile.emitCond()) { return false; } ParseNode* condNode = doNode->right(); if (!updateSourceCoordNotes(condNode->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(condNode)) { return false; } if (!doWhile.emitEnd()) { return false; } @@ -5895,16 +5995,19 @@ bool BytecodeEmitter::emitWhile(BinaryNo ParseNode* condNode = whileNode->left(); if (!wh.emitCond(getOffsetForLoop(condNode))) { return false; } if (!updateSourceCoordNotes(condNode->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(condNode)) { return false; } if (!wh.emitEnd()) { return false; } @@ -6026,16 +6129,19 @@ bool BytecodeEmitter::emitReturn(UnaryNo if (!emitPrepareIteratorResult()) { return false; } } if (!updateSourceCoordNotes(returnNode->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } /* Push a return value */ if (ParseNode* expr = returnNode->kid()) { if (!emitTree(expr)) { return false; } bool isAsyncGenerator = @@ -6738,16 +6844,19 @@ bool BytecodeEmitter::emitExpressionStat MOZ_ASSERT_IF(expr->isKind(ParseNodeKind::AssignExpr), expr->isOp(JSOP_NOP)); ValueUsage valueUsage = wantval ? ValueUsage::WantValue : ValueUsage::IgnoreValue; ExpressionStatementEmitter ese(this, valueUsage); if (!ese.prepareForExpr(Some(exprStmt->pn_pos.begin))) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(expr, valueUsage)) { return false; } if (!ese.emitEnd()) { return false; } } else if (exprStmt->isDirectivePrologueMember()) { // Don't complain about directive prologue members; just don't emit @@ -8585,16 +8694,19 @@ bool BytecodeEmitter::emitClass( // on top for EmitPropertyList, because we expect static properties to be // rarer. The result is a few more swaps than we would like. Such is life. bool isDerived = !!heritageExpression; bool hasNameOnStack = nameKind == ClassNameKind::ComputedName; if (isDerived) { if (!updateSourceCoordNotes(classNode->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitTree(heritageExpression)) { // [stack] HERITAGE return false; } if (!ce.emitDerivedClass(innerName, nameForAnonymousClass, hasNameOnStack)) { // [stack] HERITAGE HOMEOBJ return false; @@ -8737,27 +8849,33 @@ bool BytecodeEmitter::emitTree( } break; case ParseNodeKind::BreakStmt: // Ensure that the column of the 'break' is set properly. if (!updateSourceCoordNotes(pn->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitBreak(pn->as<BreakStatement>().label())) { return false; } break; case ParseNodeKind::ContinueStmt: // Ensure that the column of the 'continue' is set properly. if (!updateSourceCoordNotes(pn->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emitContinue(pn->as<ContinueStatement>().label())) { return false; } break; case ParseNodeKind::WithStmt: if (!emitWith(&pn->as<BinaryNode>())) { @@ -8932,16 +9050,19 @@ bool BytecodeEmitter::emitTree( return false; } break; case ParseNodeKind::ThrowStmt: if (!updateSourceCoordNotes(pn->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } MOZ_FALLTHROUGH; case ParseNodeKind::VoidExpr: case ParseNodeKind::NotExpr: case ParseNodeKind::BitNotExpr: case ParseNodeKind::PosExpr: case ParseNodeKind::NegExpr: if (!emitUnary(&pn->as<UnaryNode>())) { return false; @@ -9151,16 +9272,19 @@ bool BytecodeEmitter::emitTree( return false; } break; case ParseNodeKind::DebuggerStmt: if (!updateSourceCoordNotes(pn->pn_pos.begin)) { return false; } + if (!markStepBreakpoint()) { + return false; + } if (!emit1(JSOP_DEBUGGER)) { return false; } break; case ParseNodeKind::ClassDecl: if (!emitClass(&pn->as<ClassNode>())) { return false;
--- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -132,16 +132,20 @@ struct MOZ_STACK_CLASS BytecodeEmitter { // Zero-based column index on currentLine of last SRC_COLSPAN-annotated // opcode. // // WARNING: If this becomes out of sync with already-emitted srcnotes, // we can get undefined behavior. uint32_t lastColumn_; + uint32_t lastSeparatorOffet_; + uint32_t lastSeparatorLine_; + uint32_t lastSeparatorColumn_; + // switchToMain sets this to the bytecode offset of the main section. mozilla::Maybe<uint32_t> mainOffset_; public: JumpTarget lastTarget; // Last jump target emitted. // Private storage for parser wrapper. DO NOT REFERENCE INTERNALLY. May not be // initialized. Use |parser| instead. @@ -483,18 +487,21 @@ struct MOZ_STACK_CLASS BytecodeEmitter { MOZ_MUST_USE bool emitScript(ParseNode* body); // Emit function code for the tree rooted at body. enum class TopLevelFunction { No, Yes }; MOZ_MUST_USE bool emitFunctionScript(FunctionNode* funNode, TopLevelFunction isTopLevel); void updateDepth(ptrdiff_t target); + MOZ_MUST_USE bool markStepBreakpoint(); + MOZ_MUST_USE bool markSimpleBreakpoint(); MOZ_MUST_USE bool updateLineNumberNotes(uint32_t offset); MOZ_MUST_USE bool updateSourceCoordNotes(uint32_t offset); + void updateSeparatorPosition(); JSOp strictifySetNameOp(JSOp op); MOZ_MUST_USE bool emitCheck(JSOp op, ptrdiff_t delta, ptrdiff_t* offset); // Emit one bytecode. MOZ_MUST_USE bool emit1(JSOp op);
--- a/js/src/frontend/CallOrNewEmitter.cpp +++ b/js/src/frontend/CallOrNewEmitter.cpp @@ -271,16 +271,19 @@ bool CallOrNewEmitter::emitEnd(uint32_t } } } if (beginPos) { if (!bce_->updateSourceCoordNotes(*beginPos)) { return false; } } + if (!bce_->markSimpleBreakpoint()) { + return false; + } if (!isSpread()) { if (!bce_->emitCall(op_, argc)) { // [stack] RVAL return false; } } else { if (!bce_->emit1(op_)) { // [stack] RVAL
--- a/js/src/frontend/SourceNotes.h +++ b/js/src/frontend/SourceNotes.h @@ -182,18 +182,18 @@ class SrcNote { M(SRC_ASSIGNOP, "assignop", 0) /* += or another assign-op follows. */ \ M(SRC_CLASS_SPAN, "class", 2) /* The starting and ending offsets for the class, used \ for toString correctness for default ctors. */ \ M(SRC_TRY, "try", SrcNote::Try::Count) \ /* All notes above here are "gettable". See SN_IS_GETTABLE below. */ \ M(SRC_COLSPAN, "colspan", SrcNote::ColSpan::Count) \ M(SRC_NEWLINE, "newline", 0) /* Bytecode follows a source newline. */ \ M(SRC_SETLINE, "setline", SrcNote::SetLine::Count) \ - M(SRC_UNUSED22, "unused22", 0) /* Unused. */ \ - M(SRC_UNUSED23, "unused23", 0) /* Unused. */ \ + M(SRC_BREAKPOINT, "breakpoint", 0) /* Bytecode is a recommended breakpoint. */ \ + M(SRC_STEP_SEP, "step-sep", 0) /* Bytecode is the first in a new steppable area. */ \ M(SRC_XDELTA, "xdelta", 0) /* 24-31 are for extended delta notes. */ // clang-format on enum SrcNoteType { #define DEFINE_SRC_NOTE_TYPE(sym, name, arity) sym, FOR_EACH_SRC_NOTE_TYPE(DEFINE_SRC_NOTE_TYPE) #undef DEFINE_SRC_NOTE_TYPE
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetMetadata.js @@ -0,0 +1,36 @@ +var global = newGlobal({newCompartment: true}); +var dbg = Debugger(global); +dbg.onDebuggerStatement = function(frame) { + const bps = frame.script.getPossibleBreakpoints(); + + const stepBps = []; + frame.onStep = function() { + assertOffset(this); + }; + + assertOffset(frame); + + function assertOffset(frame) { + const meta = frame.script.getOffsetMetadata(frame.offset); + + if (meta.isBreakpoint) { + assertEq(frame.offset, bps[0].offset); + const expectedData = bps.shift(); + + assertEq(meta.lineNumber, expectedData.lineNumber); + assertEq(meta.columnNumber, expectedData.columnNumber); + assertEq(meta.isStepStart, expectedData.isStepStart); + } else { + assertEq(meta.isStepStart, false); + } + }; +}; + +global.eval(` + function a() { return "str"; } + debugger; + + console.log("42" + a()); + a(); + a() + a(); +`); \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints-02.js @@ -0,0 +1,63 @@ + +var global = newGlobal({newCompartment: true}); +var dbg = Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` + debugger; + function f() { + var o = {}; // 4 + + o.a; o.a; o.a; o.a; // 6 + o.a; o.a; // 7 + o.a; o.a; o.a; // 8 + o.a; // 9 + } // 10 +`); + +function onDebuggerStatement(frame) { + const fScript = frame.script.getChildScripts()[0]; + + const allBreakpoints = fScript.getPossibleBreakpoints(); + assertEq(allBreakpoints.length, 12); + + assertBPCount({ line: 4 }, 1); + assertBPCount({ line: 5 }, 0); + assertBPCount({ line: 6 }, 4); + assertBPCount({ line: 7 }, 2); + assertBPCount({ line: 8 }, 3); + assertBPCount({ line: 9 }, 1); + assertBPCount({ line: 10 }, 1); + + assertBPCount({ line: 6, minColumn: 7 }, 3); + assertBPCount({ line: 6, maxColumn: 16 }, 3); + assertBPCount({ line: 6, minColumn: 7, maxColumn: 16 }, 2); + + assertBPCount({ minLine: 9 }, 2); + assertBPCount({ minLine: 9, minColumn: 0 }, 2); + assertBPCount({ minLine: 9, minColumn: 8 }, 1); + + assertBPCount({ maxLine: 7 }, 5); + assertBPCount({ maxLine: 7, maxColumn: 0 }, 5); + assertBPCount({ maxLine: 7, maxColumn: 8 }, 6); + + assertBPCount({ minLine: 6, maxLine: 8 }, 6); + assertBPCount({ minLine: 6, minColumn: 8, maxLine: 8 }, 5); + assertBPCount({ minLine: 6, maxLine: 8, maxColumn: 8 }, 7); + assertBPCount({ minLine: 6, minColumn: 8, maxLine: 8, maxColumn: 8 }, 6); + + assertBPCount({ + minOffset: fScript.getPossibleBreakpoints({ line: 6 })[3].offset, + }, 8); + assertBPCount({ + maxOffset: fScript.getPossibleBreakpoints({ line: 6 })[3].offset, + }, 4); + assertBPCount({ + minOffset: fScript.getPossibleBreakpoints({ line: 6 })[2].offset, + maxOffset: fScript.getPossibleBreakpoints({ line: 7 })[1].offset, + }, 3); + + function assertBPCount(query, count) { + assertEq(fScript.getPossibleBreakpoints(query).length, count); + } +};
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints.js @@ -0,0 +1,391 @@ +// simple ExpressionStatement +assertBreakpoints(` + /*S*/a; + /*S*/obj.prop; +`); + +// ExpressionStatement with calls +assertBreakpoints(` + /*S*/a(); + /*S*/obj./*B*/prop(); +`); + +// ExpressionStatement with nested expression calls. +assertBreakpoints(` + "45"; + /*S*/"45" + /*B*/a(); + /*S*/b() + "45"; + /*S*/"45" + /*B*/a() + /*B*/b(); + /*S*/b() + "45" + /*B*/a(); + /*S*/b() + /*B*/a() + "45"; + + /*S*/"45" + o./*B*/a(); + /*S*/o./*B*/b() + "45"; + /*S*/"45" + o./*B*/a() + o./*B*/b(); + /*S*/o./*B*/b() + "45" + o./*B*/a(); + /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// var VariableStatement initializers +assertBreakpoints(` + var foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// let VariableStatement initializers +assertBreakpoints(` + let foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// const VariableStatement initializers +assertBreakpoints(` + const foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// EmptyStatement +assertBreakpoints(` + ; + ; + ; + /*S*/a(); +`); + +// IfStatement +assertBreakpoints(` + if (/*S*/a) {} + if (/*S*/a()) {} + if (/*S*/obj.prop) {} + if (/*S*/obj./*B*/prop()) {} + if (/*S*/"42" + a) {} + if (/*S*/"42" + /*B*/a()) {} + if (/*S*/"42" + obj.prop) {} + if (/*S*/"42" + obj./*B*/prop()) {} +`); + +// DoWhile +assertBreakpoints(` + do { + /*S*/fn(); + } while(/*S*/a) + do { + /*S*/fn(); + } while(/*S*/"42" + /*B*/a()); +`); + +// While +assertBreakpoints(` + while(/*S*/a) { + /*S*/fn(); + } + while(/*S*/"42" + /*B*/a()) { + /*S*/fn(); + } +`); + +// ForExpr +assertBreakpoints(` + for (/*S*/b = 42; /*S*/c; /*S*/d) /*S*/fn(); +`); + +// ForVar +assertBreakpoints(` + for (var b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); +`); + +// ForLet +assertBreakpoints(` + for (let b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); +`); + +// ForConst +assertBreakpoints(` + for (const b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); +`); + +// ForInExpr +assertBreakpoints(` + for (b in /*S*/d) /*S*/fn(); +`); +// ForInVar +assertBreakpoints(` + for (var b in /*S*/d) /*S*/fn(); +`); +// ForInLet +assertBreakpoints(` + for (let b in /*S*/d) /*S*/fn(); +`); +// ForInConst +assertBreakpoints(` + for (const b in /*S*/d) /*S*/fn(); +`); + +// ForOfExpr +assertBreakpoints(` + for (b of /*S*/d) /*S*/fn(); +`); +// ForOfVar +assertBreakpoints(` + for (var b of /*S*/d) /*S*/fn(); +`); +// ForOfLet +assertBreakpoints(` + for (let b of /*S*/d) /*S*/fn(); +`); +// ForOfConst +assertBreakpoints(` + for (const b of /*S*/d) /*S*/fn(); +`); + +// SwitchStatement +assertBreakpoints(` + switch (/*S*/d) { + case 42: + /*S*/fn(); + } +`); + +// ContinueStatement +assertBreakpoints(` + while (/*S*/a) { + /*S*/continue; + } +`); + +// BreakStatement +assertBreakpoints(` + while (/*S*/a) { + /*S*/break; + } +`); + +// ReturnStatement +assertBreakpoints(` + /*S*/return a + /*B*/b(); +`); + +// WithStatement +assertBreakpoints(` + with (/*S*/a) { + /*S*/fn(); + } +`); + +// ThrowStatement +assertBreakpoints(` + /*S*/throw /*B*/fn(); + /*S*/throw "42" + /*B*/fn(); +`); + +// DebuggerStatement +assertBreakpoints(` + /*S*/debugger; + /*S*/debugger; +`); + +// BlockStatent wrapper +assertBreakpoints(` + { + /*S*/a(); + } +`); + +// ClassDeclaration +assertBreakpoints(` + class Foo2 {} + /*S*/class Foo extends ("" + o.a + /*B*/a() + /*B*/b()) { } +`); + +// Misc examples +assertBreakpoints(` + /*S*/void /*B*/a(); +`); +assertBreakpoints(` + /*S*/a() + /*B*/b(); +`); +assertBreakpoints(` + for ( + var i = /*S*/0; + /*S*/i < n; // 4 + /*S*/++i + ) { + /*S*/console./*B*/log("omg"); + } +`); +assertBreakpoints(` + function * gen(){ + var foo = ( + (/*S*/console./*B*/log('before', /*B*/a())), + (yield console./*B*/log('mid', /*B*/b())), + (console./*B*/log('after', /*B*/a())) + ); + var foo2 = /*S*/a() + /*B*/b(); + /*S*/console./*B*/log(foo); + /*B*/} + var i = /*S*/0; + for (var foo of /*S*/gen()) { + /*S*/console./*B*/log(i++); + } +`); +assertBreakpoints(` + var fn /*S*/= () => { + /*S*/console./*B*/log("fn"); + /*S*/return /*B*/new Proxy({ prop: 42 }, { + deleteProperty() { + /*S*/console./*B*/log("delete"); + /*B*/} + }); + /*B*/}; +`); +assertBreakpoints(` + if ((/*S*/delete /*B*/fn().prop) + /*B*/b()) { + /*S*/console./*B*/log("foo"); + } +`); +assertBreakpoints(` + for (var j = /*S*/0; (/*S*/o.a) < 3; (/*S*/j++, /*B*/a(), /*B*/b())) { + /*S*/console./*B*/log(i); + } +`); +assertBreakpoints(` + function fn2( + [a, b] = (/*B*/a(), /*B*/b()) + ) { + /*S*/a(); + /*S*/b(); + /*B*/} + + ({ a, b } = (/*S*/a(), /*B*/b())); +`); +assertBreakpoints(` + /*S*/o.a + "42" + /*B*/a() + /*B*/b(); +`); +assertBreakpoints(` + /*S*/a(); + /*S*/o./*B*/a(/*B*/b()); +`); +assertBreakpoints(` + (/*S*/{}[obj.a] = 42 + /*B*/a()); +`); +assertBreakpoints(` + var { + foo = o.a + } = /*S*/{}; +`); +assertBreakpoints(` + var ack = /*S*/[ + o.a, + o.b, + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + ]; +`); + +function assertBreakpoints(expected) { + const input = expected.replace(/\/\*[BS]\*\//g, ""); + + var global = newGlobal({newCompartment: true}); + var dbg = Debugger(global); + dbg.onDebuggerStatement = function(frame) { + const fScript = frame.environment.parent.getVariable("f").script; + + let positions = []; + (function recurse(script) { + const bps = script.getPossibleBreakpoints(); + const offsets = script.getPossibleBreakpointOffsets(); + + assertEq(offsets.length, bps.length); + for (let i = 0; i < bps.length; i++) { + assertEq(offsets[i], bps[i].offset); + } + + positions = positions.concat(bps); + script.getChildScripts().forEach(recurse); + })(fScript); + + const result = annotateOffsets(input, positions); + assertEq(result, expected + "/*B*/"); + }; + + global.eval(`function f(){${input}} debugger;`); +} + +function annotateOffsets(code, positions) { + const offsetLookup = createOffsetLookup(code); + + positions = positions.slice(); + positions.sort((a, b) => { + const lineDiff = a.lineNumber - b.lineNumber; + return lineDiff === 0 ? a.columnNumber - b.columnNumber : lineDiff; + }); + positions.reverse(); + + let output = ""; + let last = code.length; + for (const { lineNumber, columnNumber, isStepStart } of positions) { + const offset = offsetLookup(lineNumber, columnNumber); + + output = "/*" + (isStepStart ? "S" : "B") + "*/" + code.slice(offset, last) + output; + last = offset; + } + return code.slice(0, last) + output; +} + +function createOffsetLookup(code) { + const lines = code.split(/(\r?\n|\r|\u2028|\u2029)/g); + const lineOffsets = []; + + let count = 0; + for (const [i, str] of lines.entries()) { + if (i % 2 === 0) { + lineOffsets[i / 2] = count; + } + count += str.length; + } + + return function(line, column) { + // Lines from getAllColumnOffsets are 1-based. + line = line - 1; + + if (!lineOffsets.hasOwnProperty(line)) { + throw new Error("Unknown line " + line + " column " + column); + } + return lineOffsets[line] + column; + }; +}
--- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -2964,16 +2964,18 @@ static MOZ_MUST_USE bool SrcNotes(JSCont case SRC_IF: case SRC_IF_ELSE: case SRC_COND: case SRC_CONTINUE: case SRC_BREAK: case SRC_BREAK2LABEL: case SRC_SWITCHBREAK: case SRC_ASSIGNOP: + case SRC_BREAKPOINT: + case SRC_STEP_SEP: case SRC_XDELTA: break; case SRC_COLSPAN: colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, SrcNote::ColSpan::Span)); if (!sp->jsprintf("%d", colspan)) { return false;
--- a/js/src/vm/BytecodeUtil-inl.h +++ b/js/src/vm/BytecodeUtil-inl.h @@ -113,16 +113,18 @@ class BytecodeRangeWithPosition : privat BytecodeRangeWithPosition(JSContext* cx, JSScript* script) : BytecodeRange(cx, script), lineno(script->lineno()), column(0), sn(script->notes()), snpc(script->code()), isEntryPoint(false), + isBreakpoint(false), + seenStepSeparator(false), wasArtifactEntryPoint(false) { if (!SN_IS_TERMINATOR(sn)) { snpc += SN_DELTA(sn); } updatePosition(); while (frontPC() != script->main()) { popFront(); } @@ -163,18 +165,33 @@ class BytecodeRangeWithPosition : privat // explicit mention in the line table. This restriction avoids a // number of failing cases caused by some instructions not having // sensible (to the user) line numbers, and it is one way to // implement the idea that the bytecode emitter should tell the // debugger exactly which offsets represent "interesting" (to the // user) places to stop. bool frontIsEntryPoint() const { return isEntryPoint; } + // Breakable points are explicitly marked by the emitter as locations where + // the debugger may want to allow users to pause. + bool frontIsBreakablePoint() const { return isBreakpoint; } + + // Breakable step points are the first breakable point after a SRC_STEP_SEP + // note has been encountered. + bool frontIsBreakableStepPoint() const { + return isBreakpoint && seenStepSeparator; + } + private: void updatePosition() { + if (isBreakpoint) { + isBreakpoint = false; + seenStepSeparator = false; + } + // Determine the current line number by reading all source notes up to // and including the current offset. jsbytecode* lastLinePC = nullptr; while (!SN_IS_TERMINATOR(sn) && snpc <= frontPC()) { SrcNoteType type = SN_TYPE(sn); if (type == SRC_COLSPAN) { ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, SrcNote::ColSpan::Span)); @@ -184,27 +201,35 @@ class BytecodeRangeWithPosition : privat } else if (type == SRC_SETLINE) { lineno = size_t(GetSrcNoteOffset(sn, SrcNote::SetLine::Line)); column = 0; lastLinePC = snpc; } else if (type == SRC_NEWLINE) { lineno++; column = 0; lastLinePC = snpc; + } else if (type == SRC_BREAKPOINT) { + isBreakpoint = true; + lastLinePC = snpc; + } else if (type == SRC_STEP_SEP) { + seenStepSeparator = true; + lastLinePC = snpc; } sn = SN_NEXT(sn); snpc += SN_DELTA(sn); } isEntryPoint = lastLinePC == frontPC(); } size_t lineno; size_t column; jssrcnote* sn; jsbytecode* snpc; bool isEntryPoint; + bool isBreakpoint; + bool seenStepSeparator; bool wasArtifactEntryPoint; }; } // namespace js #endif /* vm_BytecodeUtil_inl_h */
--- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -212,21 +212,23 @@ MACRO(Int8x16, Int8x16, "Int8x16") \ MACRO(Int16x8, Int16x8, "Int16x8") \ MACRO(Int32x4, Int32x4, "Int32x4") \ MACRO(integer, integer, "integer") \ MACRO(interface, interface, "interface") \ MACRO(InterpretGeneratorResume, InterpretGeneratorResume, \ "InterpretGeneratorResume") \ MACRO(InvalidDate, InvalidDate, "Invalid Date") \ + MACRO(isBreakpoint, isBreakpoint, "isBreakpoint") \ MACRO(isEntryPoint, isEntryPoint, "isEntryPoint") \ MACRO(isExtensible, isExtensible, "isExtensible") \ MACRO(isFinite, isFinite, "isFinite") \ MACRO(isNaN, isNaN, "isNaN") \ MACRO(isPrototypeOf, isPrototypeOf, "isPrototypeOf") \ + MACRO(isStepStart, isStepStart, "isStepStart") \ MACRO(IterableToList, IterableToList, "IterableToList") \ MACRO(iterate, iterate, "iterate") \ MACRO(join, join, "join") \ MACRO(js, js, "js") \ MACRO(keys, keys, "keys") \ MACRO(label, label, "label") \ MACRO(lastIndex, lastIndex, "lastIndex") \ MACRO(length, length, "length") \ @@ -236,26 +238,32 @@ MACRO(literal, literal, "literal") \ MACRO(loc, loc, "loc") \ MACRO(locale, locale, "locale") \ MACRO(lookupGetter, lookupGetter, "__lookupGetter__") \ MACRO(lookupSetter, lookupSetter, "__lookupSetter__") \ MACRO(ltr, ltr, "ltr") \ MACRO(MapConstructorInit, MapConstructorInit, "MapConstructorInit") \ MACRO(MapIterator, MapIterator, "Map Iterator") \ + MACRO(maxColumn, maxColumn, "maxColumn") \ MACRO(maximumFractionDigits, maximumFractionDigits, "maximumFractionDigits") \ MACRO(maximumSignificantDigits, maximumSignificantDigits, \ "maximumSignificantDigits") \ + MACRO(maxLine, maxLine, "maxLine") \ + MACRO(maxOffset, maxOffset, "maxOffset") \ MACRO(message, message, "message") \ MACRO(meta, meta, "meta") \ + MACRO(minColumn, minColumn, "minColumn") \ MACRO(minDays, minDays, "minDays") \ MACRO(minimumFractionDigits, minimumFractionDigits, "minimumFractionDigits") \ MACRO(minimumIntegerDigits, minimumIntegerDigits, "minimumIntegerDigits") \ MACRO(minimumSignificantDigits, minimumSignificantDigits, \ "minimumSignificantDigits") \ + MACRO(minLine, minLine, "minLine") \ + MACRO(minOffset, minOffset, "minOffset") \ MACRO(minusSign, minusSign, "minusSign") \ MACRO(minute, minute, "minute") \ MACRO(missingArguments, missingArguments, "missingArguments") \ MACRO(mode, mode, "mode") \ MACRO(module, module, "module") \ MACRO(Module, Module, "Module") \ MACRO(ModuleInstantiate, ModuleInstantiate, "ModuleInstantiate") \ MACRO(ModuleEvaluate, ModuleEvaluate, "ModuleEvaluate") \
--- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -6184,16 +6184,488 @@ static bool EnsureScriptOffsetIsValid(JS if (IsValidBytecodeOffset(cx, script, offset)) { return true; } JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_OFFSET); return false; } +template <bool OnlyOffsets> +class DebuggerScriptGetPossibleBreakpointsMatcher { + JSContext* cx_; + MutableHandleObject result_; + + Maybe<size_t> minOffset; + Maybe<size_t> maxOffset; + + Maybe<size_t> minLine; + size_t minColumn; + Maybe<size_t> maxLine; + size_t maxColumn; + + bool passesQuery(size_t offset, size_t lineno, size_t 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, size_t lineno, size_t colno, + bool isStepStart) { + if (!passesQuery(offset, lineno, colno)) { + return true; + } + + if (OnlyOffsets) { + if (!NewbornArrayPush(cx_, result_, NumberValue(offset))) { + return false; + } + + return true; + } + + RootedPlainObject entry(cx_, NewBuiltinClassInstance<PlainObject>(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); + 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; + } + + bool parseIntValue(HandleValue value, size_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 parseIntValue(HandleValue value, Maybe<size_t>* result) { + size_t result_; + if (!parseIntValue(value, &result_)) { + return false; + } + + *result = Some(result_); + return true; + } + + public: + explicit DebuggerScriptGetPossibleBreakpointsMatcher( + JSContext* cx, MutableHandleObject result) + : cx_(cx), + result_(result), + minOffset(), + maxOffset(), + minLine(), + minColumn(0), + maxLine(), + maxColumn(0) {} + + 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 (!parseIntValue(minOffsetValue, &minOffset)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minOffset'", "not an integer"); + return false; + } + } + if (!maxOffsetValue.isUndefined()) { + if (!parseIntValue(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'"); + } + + size_t line; + if (!parseIntValue(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 (!parseIntValue(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 (!parseIntValue(minColumnValue, &minColumn)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'minColumn'", "not an integer"); + return false; + } + } + + if (!maxLineValue.isUndefined()) { + if (!parseIntValue(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 (!parseIntValue(maxColumnValue, &maxColumn)) { + JS_ReportErrorNumberASCII( + cx_, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "getPossibleBreakpoints' 'maxColumn'", "not an integer"); + return false; + } + } + + return true; + } + + using ReturnType = bool; + ReturnType match(HandleScript script) { + // 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(); + size_t lineno = r.frontLineNumber(); + size_t colno = r.frontColumnNumber(); + + if (!maybeAppendEntry(offset, lineno, colno, + r.frontIsBreakableStepPoint())) { + return false; + } + } + + return true; + } + ReturnType match(Handle<LazyScript*> lazyScript) { + RootedScript script(cx_, DelazifyScript(cx_, lazyScript)); + if (!script) { + return false; + } + return match(script); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + + Vector<wasm::ExprLoc> offsets(cx_); + if (instance.debugEnabled() && + !instance.debug().getAllColumnOffsets(cx_, &offsets)) { + return false; + } + + result_.set(NewDenseEmptyArray(cx_)); + if (!result_) { + return false; + } + + for (uint32_t i = 0; i < offsets.length(); i++) { + size_t lineno = offsets[i].lineno; + size_t column = offsets[i].column; + size_t offset = offsets[i].offset; + if (!maybeAppendEntry(offset, lineno, column, true)) { + return false; + } + } + return true; + } +}; + +static bool DebuggerScript_getPossibleBreakpoints(JSContext* cx, unsigned argc, + Value* vp) { + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getPossibleBreakpoints", args, obj, + referent); + + RootedObject result(cx); + DebuggerScriptGetPossibleBreakpointsMatcher<false> matcher(cx, &result); + if (args.length() >= 1 && !args[0].isUndefined()) { + RootedObject queryObject(cx, NonNullObject(cx, args[0])); + if (!queryObject || !matcher.parseQuery(queryObject)) { + return false; + } + } + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +static bool DebuggerScript_getPossibleBreakpointOffsets(JSContext* cx, + unsigned argc, + Value* vp) { + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getPossibleBreakpointOffsets", args, + obj, referent); + + RootedObject result(cx); + DebuggerScriptGetPossibleBreakpointsMatcher<true> matcher(cx, &result); + if (args.length() >= 1 && !args[0].isUndefined()) { + RootedObject queryObject(cx, NonNullObject(cx, args[0])); + if (!queryObject || !matcher.parseQuery(queryObject)) { + return false; + } + } + if (!referent.match(matcher)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +class DebuggerScriptGetOffsetMetadataMatcher { + JSContext* cx_; + size_t offset_; + MutableHandlePlainObject result_; + + public: + explicit DebuggerScriptGetOffsetMetadataMatcher( + JSContext* cx, size_t offset, MutableHandlePlainObject result) + : cx_(cx), offset_(offset), result_(result) {} + using ReturnType = bool; + ReturnType match(HandleScript script) { + if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) { + return false; + } + + result_.set(NewBuiltinClassInstance<PlainObject>(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()); + 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<LazyScript*> lazyScript) { + RootedScript script(cx_, DelazifyScript(cx_, lazyScript)); + if (!script) { + return false; + } + return match(script); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + size_t lineno; + size_t column; + if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) { + JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, + JSMSG_DEBUG_BAD_OFFSET); + return false; + } + + result_.set(NewBuiltinClassInstance<PlainObject>(cx_)); + if (!result_) { + return false; + } + + RootedValue value(cx_, NumberValue(lineno)); + if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { + return false; + } + + value = NumberValue(column); + 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; + } +}; + +static bool DebuggerScript_getOffsetMetadata(JSContext* cx, unsigned argc, + Value* vp) { + THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getOffsetMetadata", args, obj, + referent); + if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetMetadata", 1)) { + return false; + } + size_t offset; + if (!ScriptOffset(cx, args[0], &offset)) { + return false; + } + + RootedPlainObject result(cx); + DebuggerScriptGetOffsetMetadataMatcher 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 @@ -7483,28 +7955,37 @@ static const JSPropertySpec DebuggerScri JS_PSG("sourceLength", DebuggerScript_getSourceLength, 0), JS_PSG("mainOffset", DebuggerScript_getMainOffset, 0), JS_PSG("global", DebuggerScript_getGlobal, 0), JS_PSG("format", DebuggerScript_getFormat, 0), JS_PS_END}; static const JSFunctionSpec DebuggerScript_methods[] = { JS_FN("getChildScripts", DebuggerScript_getChildScripts, 0, 0), - JS_FN("getAllOffsets", DebuggerScript_getAllOffsets, 0, 0), - JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0), - JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0), - JS_FN("getOffsetLocation", DebuggerScript_getOffsetLocation, 0, 0), - JS_FN("getSuccessorOffsets", DebuggerScript_getSuccessorOffsets, 1, 0), - JS_FN("getPredecessorOffsets", DebuggerScript_getPredecessorOffsets, 1, 0), + JS_FN("getPossibleBreakpoints", DebuggerScript_getPossibleBreakpoints, 0, + 0), + JS_FN("getPossibleBreakpointOffsets", + DebuggerScript_getPossibleBreakpointOffsets, 0, 0), JS_FN("setBreakpoint", DebuggerScript_setBreakpoint, 2, 0), JS_FN("getBreakpoints", DebuggerScript_getBreakpoints, 1, 0), JS_FN("clearBreakpoint", DebuggerScript_clearBreakpoint, 1, 0), JS_FN("clearAllBreakpoints", DebuggerScript_clearAllBreakpoints, 0, 0), JS_FN("isInCatchScope", DebuggerScript_isInCatchScope, 1, 0), + JS_FN("getOffsetMetadata", DebuggerScript_getOffsetMetadata, 1, 0), JS_FN("getOffsetsCoverage", DebuggerScript_getOffsetsCoverage, 0, 0), + JS_FN("getSuccessorOffsets", DebuggerScript_getSuccessorOffsets, 1, 0), + JS_FN("getPredecessorOffsets", DebuggerScript_getPredecessorOffsets, 1, 0), + + // The following APIs are deprecated due to their reliance on the + // under-defined 'entrypoint' concept. Make use of getPossibleBreakpoints, + // getPossibleBreakpointOffsets, or getOffsetMetadata instead. + JS_FN("getAllOffsets", DebuggerScript_getAllOffsets, 0, 0), + JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0), + JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0), + JS_FN("getOffsetLocation", DebuggerScript_getOffsetLocation, 0, 0), JS_FS_END}; /*** Debugger.Source ********************************************************/ // For internal use only. static inline NativeObject* GetSourceReferentRawObject(JSObject* obj) { MOZ_ASSERT(obj->getClass() == &DebuggerSource_class); return static_cast<NativeObject*>(obj->as<NativeObject>().getPrivate());