[lldb] Define breakpoint location "." to mean the location(s) at which the current thread is stopped (#194272)

Adds `.` as a new `breakpt-id` syntax. Users can specify `.` to mean the breakpoint
location(s) that caused the current thread to stop.

I selected `.` to mean the current breakpoint locations for two reasons. In a shells,
period means <ins>current</ins> directory. In prose, a period is a <ins>stop</ins>.

My workflow often starts with multiple breakpoint locations, such as with regex
breakpoints, or basename breakpoints for overloaded/overridden names. As locations are
hit, I realize which locations are no longer needed. This new syntax makes it quick and
easy to disable the currently stopped location(s).

Another use case for this is to quickly repeat commands for the current location:

``` break com add -o 'p someVar' .  ```

Usage example:

``` (lldb) b main.c:2 Process 47071 stopped * thread #1, queue =
'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: ... main`main at main.c:2:3
   1    int main() {
-> 2      return 0;
   3    }
Target 0: (main) stopped.  (lldb) breakpoint disable .  1 breakpoints disabled.  (lldb)
breakpoint list Current breakpoints: 1: file = 'main.c', line = 2, exact_match = 0,
locations = 1
  1.1: where = main`main + 12 at main.c:2:3, address = ..., hit count = 1  Options:
  disabled
```

rdar://73047170

Assisted-by: claude
This commit is contained in:
Dave Lee
2026-04-28 14:42:49 -07:00
committed by GitHub
parent d394d153fb
commit 749af7f06d
11 changed files with 132 additions and 28 deletions

View File

@@ -50,10 +50,9 @@ public:
static std::pair<llvm::StringRef, llvm::StringRef>
SplitIDRangeExpression(llvm::StringRef in_string);
static llvm::Error
FindAndReplaceIDRanges(Args &old_args, Target *target, bool allow_locations,
BreakpointName::Permissions ::PermissionKinds purpose,
Args &new_args);
static llvm::Error FindAndReplaceIDRanges(
Args &old_args, ExecutionContext &exe_ctx, bool allow_locations,
BreakpointName::Permissions ::PermissionKinds purpose, Args &new_args);
private:
BreakpointIDArray m_breakpoint_ids;

View File

@@ -11,9 +11,13 @@
#include "lldb/Breakpoint/Breakpoint.h"
#include "lldb/Breakpoint/BreakpointLocation.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Target/StopInfo.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Utility/Args.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/lldb-forward.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h"
@@ -55,6 +59,16 @@ bool BreakpointIDList::Contains(BreakpointID bp_id) const {
return llvm::is_contained(m_breakpoint_ids, bp_id);
}
static std::string LocationIDForStop(StopInfoSP stop_info_sp, uint32_t idx) {
assert(stop_info_sp->GetStopReason() == lldb::eStopReasonBreakpoint &&
"expected breakpoint stop");
break_id_t bp_id = stop_info_sp->GetStopReasonDataAtIndex(idx);
break_id_t loc_id = stop_info_sp->GetStopReasonDataAtIndex(idx + 1);
StreamString stream;
BreakpointID::GetCanonicalReference(&stream, bp_id, loc_id);
return stream.GetString().str();
}
// This function takes OLD_ARGS, which is usually the result of breaking the
// command string arguments into
// an array of space-separated strings, and searches through the arguments for
@@ -69,8 +83,9 @@ bool BreakpointIDList::Contains(BreakpointID bp_id) const {
// by the members of the range.
llvm::Error BreakpointIDList::FindAndReplaceIDRanges(
Args &old_args, Target *target, bool allow_locations,
Args &old_args, ExecutionContext &exe_ctx, bool allow_locations,
BreakpointName::Permissions ::PermissionKinds purpose, Args &new_args) {
Target *target = exe_ctx.GetTargetPtr();
llvm::StringRef range_from;
llvm::StringRef range_to;
llvm::StringRef current_arg;
@@ -80,6 +95,30 @@ llvm::Error BreakpointIDList::FindAndReplaceIDRanges(
bool is_range = false;
current_arg = old_args[i].ref();
if (allow_locations && current_arg == ".") {
Thread *thread = exe_ctx.GetThreadPtr();
if (!thread) {
new_args.Clear();
return llvm::createStringError("no current thread");
}
StopInfoSP stop_info_sp = thread->GetStopInfo();
if (!stop_info_sp ||
stop_info_sp->GetStopReason() != eStopReasonBreakpoint) {
new_args.Clear();
return llvm::createStringError(
"current thread is not stopped at a breakpoint");
}
uint32_t data_count = stop_info_sp->GetStopReasonDataCount();
for (uint32_t j = 0; j < data_count; j += 2) {
std::string location_id = LocationIDForStop(stop_info_sp, j);
new_args.AppendArgument(location_id);
}
continue;
}
if (!allow_locations && current_arg.contains('.')) {
new_args.Clear();
return llvm::createStringError(

View File

@@ -2073,7 +2073,7 @@ protected:
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::disablePerm);
if (result.Succeeded()) {
@@ -2153,7 +2153,7 @@ protected:
// Particular breakpoint selected; enable that breakpoint.
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::disablePerm);
if (result.Succeeded()) {
@@ -2261,7 +2261,7 @@ protected:
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::disablePerm);
if (result.Succeeded()) {
@@ -2406,7 +2406,7 @@ protected:
// Particular breakpoints selected; show info about that breakpoint.
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
if (result.Succeeded()) {
@@ -2685,7 +2685,7 @@ protected:
if (!command.empty()) {
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &excluded_bp_ids,
command, m_exe_ctx, result, &excluded_bp_ids,
BreakpointName::Permissions::PermissionKinds::deletePerm);
if (!result.Succeeded())
return;
@@ -2704,7 +2704,7 @@ protected:
}
} else {
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::deletePerm);
if (!result.Succeeded())
return;
@@ -3015,7 +3015,7 @@ protected:
// Particular breakpoint selected; disable that breakpoint.
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
if (result.Succeeded()) {
@@ -3089,7 +3089,7 @@ protected:
// Particular breakpoint selected; disable that breakpoint.
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::deletePerm);
if (result.Succeeded()) {
@@ -3562,7 +3562,7 @@ protected:
BreakpointIDList valid_bp_ids;
if (!command.empty()) {
CommandObjectMultiwordBreakpoint::VerifyBreakpointIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
if (!result.Succeeded()) {
@@ -3648,7 +3648,7 @@ CommandObjectMultiwordBreakpoint::CommandObjectMultiwordBreakpoint(
CommandObjectMultiwordBreakpoint::~CommandObjectMultiwordBreakpoint() = default;
void CommandObjectMultiwordBreakpoint::VerifyIDs(
Args &args, Target &target, bool allow_locations,
Args &args, ExecutionContext &exe_ctx, bool allow_locations,
CommandReturnObject &result, BreakpointIDList *valid_ids,
BreakpointName::Permissions ::PermissionKinds purpose) {
// args can be strings representing 1). integers (for breakpoint ids)
@@ -3662,6 +3662,7 @@ void CommandObjectMultiwordBreakpoint::VerifyIDs(
// If args is empty, we will use the last created breakpoint (if there is
// one.)
Target &target = exe_ctx.GetTargetRef();
Args temp_args;
if (args.empty()) {
@@ -3683,7 +3684,7 @@ void CommandObjectMultiwordBreakpoint::VerifyIDs(
// into TEMP_ARGS.
if (llvm::Error err = BreakpointIDList::FindAndReplaceIDRanges(
args, &target, allow_locations, purpose, temp_args)) {
args, exe_ctx, allow_locations, purpose, temp_args)) {
result.SetError(std::move(err));
return;
}

View File

@@ -23,22 +23,22 @@ public:
~CommandObjectMultiwordBreakpoint() override;
static void VerifyBreakpointOrLocationIDs(
Args &args, Target &target, CommandReturnObject &result,
Args &args, ExecutionContext &exe_ctx, CommandReturnObject &result,
BreakpointIDList *valid_ids,
BreakpointName::Permissions ::PermissionKinds purpose) {
VerifyIDs(args, target, true, result, valid_ids, purpose);
VerifyIDs(args, exe_ctx, true, result, valid_ids, purpose);
}
static void
VerifyBreakpointIDs(Args &args, Target &target, CommandReturnObject &result,
BreakpointIDList *valid_ids,
VerifyBreakpointIDs(Args &args, ExecutionContext &exe_ctx,
CommandReturnObject &result, BreakpointIDList *valid_ids,
BreakpointName::Permissions::PermissionKinds purpose) {
VerifyIDs(args, target, false, result, valid_ids, purpose);
VerifyIDs(args, exe_ctx, false, result, valid_ids, purpose);
}
private:
static void VerifyIDs(Args &args, Target &target, bool allow_locations,
CommandReturnObject &result,
static void VerifyIDs(Args &args, ExecutionContext &exe_ctx,
bool allow_locations, CommandReturnObject &result,
BreakpointIDList *valid_ids,
BreakpointName::Permissions::PermissionKinds purpose);
};

View File

@@ -344,7 +344,7 @@ protected:
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
m_bp_options_vec.clear();
@@ -500,7 +500,7 @@ protected:
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
if (result.Succeeded()) {
@@ -567,7 +567,7 @@ protected:
BreakpointIDList valid_bp_ids;
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
command, target, result, &valid_bp_ids,
command, m_exe_ctx, result, &valid_bp_ids,
BreakpointName::Permissions::PermissionKinds::listPerm);
if (result.Succeeded()) {

View File

@@ -533,7 +533,7 @@ protected:
// default breakpoint.
if (m_options.m_run_to_bkpt_args.GetArgumentCount() > 0)
CommandObjectMultiwordBreakpoint::VerifyBreakpointOrLocationIDs(
m_options.m_run_to_bkpt_args, target, result, &run_to_bkpt_ids,
m_options.m_run_to_bkpt_args, m_exe_ctx, result, &run_to_bkpt_ids,
BreakpointName::Permissions::disablePerm);
if (!result.Succeeded()) {
return;

View File

@@ -49,7 +49,10 @@ llvm::StringRef BreakpointIDHelpTextCallback() {
"major "
"number, or the major number followed by a dot and the location "
"number (e.g. "
"3 or 3.2 could both be valid breakpoint IDs.)";
"3 or 3.2 could both be valid breakpoint IDs.)\n"
"\n"
"You can use . to refer to the breakpoint location(s) at which the "
"current thread is stopped.";
}
llvm::StringRef BreakpointIDRangeHelpTextCallback() {

View File

@@ -0,0 +1,3 @@
C_SOURCES := main.c
include Makefile.rules

View File

@@ -0,0 +1,51 @@
import lldb
from lldbsuite.test.lldbtest import TestBase
from lldbsuite.test import lldbutil
class TestCase(TestBase):
def test_disable_enable(self):
self.build()
_, _, _, bp = lldbutil.run_to_source_breakpoint(
self, "break here", lldb.SBFileSpec("main.c")
)
self.assertTrue(bp.FindLocationByID(1).IsEnabled())
self.expect("breakpoint disable .", startstr="1 breakpoints disabled.")
self.assertFalse(bp.FindLocationByID(1).IsEnabled())
self.expect("breakpoint enable .", startstr="1 breakpoints enabled.")
self.assertTrue(bp.FindLocationByID(1).IsEnabled())
def test_delete(self):
self.build()
_, _, _, bp = lldbutil.run_to_source_breakpoint(
self, "break here", lldb.SBFileSpec("main.c")
)
self.expect(
"breakpoint delete .",
startstr="0 breakpoints deleted; 1 breakpoint locations disabled",
)
self.assertFalse(bp.FindLocationByID(1).IsEnabled())
def test_error_not_breakpoint_stop(self):
self.build()
_, _, thread, bp = lldbutil.run_to_source_breakpoint(
self, "break here", lldb.SBFileSpec("main.c")
)
self.assertTrue(bp.FindLocationByID(1).IsEnabled())
thread.StepOver()
self.assertNotEqual(thread.stop_reason, lldb.eStopReasonBreakpoint)
self.expect(
"breakpoint disable .",
error=True,
startstr="error: current thread is not stopped at a breakpoint",
)
self.assertTrue(bp.FindLocationByID(1).IsEnabled())
def test_error_no_process(self):
self.build()
target = self.createTestTarget()
target.BreakpointCreateByLocation("main.c", 2)
self.expect("breakpoint disable .", error=True, substrs=["no current thread"])

View File

@@ -0,0 +1,5 @@
int main() {
int x = 1; // break here
int y = 2;
return x + y;
}

View File

@@ -259,6 +259,9 @@ Changes to LLDB
code signed dSYM bundles are now loaded automatically, while untrusted bundles continue to produce a warning.
* Pressing enter after `frame variable` repeats the command with an incremented `--depth` option, allowing quick
expansion of nested data.
* Breakpoint commands now accept `.` to refer to the location(s) at which the current thread is stopped. For
example, `breakpoint disable .` disables the just-hit breakpoint location. Another usage is to automate a
command to run at the current location: `breakpoint command add -o 'p my_var' .`.
### Deprecated APIs