Bug 1518661 - Part 5: Give SpiderMonkey well-defined sense of step and breakpoint locations. r=jimb,bhackett
authorLogan Smyth <loganfsmyth@gmail.com>
Wed, 13 Feb 2019 02:31:00 +0000
changeset 458905 5c934ede1cfc
parent 458904 5add2761a3b6
child 458906 62f3c188b868
push id35551
push usershindli@mozilla.com
push dateWed, 13 Feb 2019 21:34:09 +0000
treeherdermozilla-central@08f794a4928e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb, bhackett
bugs1518661
milestone67.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
Bug 1518661 - Part 5: Give SpiderMonkey well-defined sense of step and breakpoint locations. r=jimb,bhackett Until now, SpiderMonkey's debugger interfaces have generally relied on the implicitly-defined 'entrypoint' concept. This meant that assigning a given bytecode a position also automatically meant that it behaved like a breakpoint. This both made it hard to maintain, and hard to define user's expectations because there could potentially be many more breakpoints than users would actually want. This patch adds an official concept of recommended breakpoint and recommended step-next pause locations, and APIs for users to query for them. The expectation being that users would now use this metadata in debugging UIs. Differential Revision: https://phabricator.services.mozilla.com/D15994
js/src/doc/Debugger/Debugger.Script.md
js/src/frontend/BytecodeEmitter.cpp
js/src/frontend/BytecodeEmitter.h
js/src/frontend/CallOrNewEmitter.cpp
js/src/frontend/SourceNotes.h
js/src/jit-test/tests/debug/Script-getOffsetMetadata.js
js/src/jit-test/tests/debug/Script-getPossibleBreakpoints-02.js
js/src/jit-test/tests/debug/Script-getPossibleBreakpoints.js
js/src/shell/js.cpp
js/src/vm/BytecodeUtil-inl.h
js/src/vm/CommonPropertyNames.h
js/src/vm/Debugger.cpp
--- 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());