[Lldb-commits] [lldb] [lldb] Make thread until a step out plan that stops at the until points (PR #228175)

Felipe de Azevedo Piovezan via lldb-commits lldb-commits at lists.llvm.org
Thu Oct 1 11:01:35 PDT 2026


https://github.com/felipepiovezan created https://github.com/llvm/llvm-project/pull/228175

`thread until -f N <target>` put a backstop breakpoint at the PC of frame N+1. When frame N+1 is an inline frame, that PC is the start of the inlined code of frame N, which already ran. The backstop is never hit, and the process runs to exit if the target is not reached.

The plan also completed as "stepped out" at every hit of the backstop. In a recursive function, the recursive calls of frame N return to the address of the backstop, so the plan stopped in frame N.

Now ThreadPlanStepUntil is a ThreadPlanStepOut that steps out of frame N and completes early when an until breakpoint is hit in frame N.

The until breakpoints stay enabled while the plans that ThreadPlanStepOut queues run above the until plan.

>From 0523f9ed4f7127fca333c441cc45da96e2f6a63a Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Tue, 29 Sep 2026 10:29:16 +0100
Subject: [PATCH] [lldb] Make thread until a step out plan that stops at the
 until points

`thread until -f N <target>` put a backstop breakpoint at the PC of
frame N+1. When frame N+1 is an inline frame, that PC is the start of the
inlined code of frame N, which already ran. The backstop is never hit,
and the process runs to exit if the target is not reached.

The plan also completed as "stepped out" at every hit of the backstop.
In a recursive function, the recursive calls of frame N return to the
address of the backstop, so the plan stopped in frame N.

Now ThreadPlanStepUntil is a ThreadPlanStepOut that steps out of frame N
and completes early when an until breakpoint is hit in frame N.

The until breakpoints stay enabled while the plans that ThreadPlanStepOut
queues run above the until plan.
---
 lldb/include/lldb/Target/ThreadPlanStepOut.h  |   7 +
 .../include/lldb/Target/ThreadPlanStepUntil.h |  22 +-
 lldb/source/Target/ThreadPlanStepOut.cpp      |  15 +-
 lldb/source/Target/ThreadPlanStepUntil.cpp    | 246 ++++--------------
 .../inline-frame-pc/TestInlineFrameUntil.py   |  19 ++
 .../functionalities/inline-frame-pc/main.c    |   4 +
 .../thread/step_until_recursive/Makefile      |   3 +
 .../TestStepUntilRecursive.py                 |  27 ++
 .../thread/step_until_recursive/main.c        |  14 +
 9 files changed, 151 insertions(+), 206 deletions(-)
 create mode 100644 lldb/test/API/functionalities/thread/step_until_recursive/Makefile
 create mode 100644 lldb/test/API/functionalities/thread/step_until_recursive/TestStepUntilRecursive.py
 create mode 100644 lldb/test/API/functionalities/thread/step_until_recursive/main.c

diff --git a/lldb/include/lldb/Target/ThreadPlanStepOut.h b/lldb/include/lldb/Target/ThreadPlanStepOut.h
index ad4434be14120..42a447002103c 100644
--- a/lldb/include/lldb/Target/ThreadPlanStepOut.h
+++ b/lldb/include/lldb/Target/ThreadPlanStepOut.h
@@ -51,6 +51,13 @@ class ThreadPlanStepOut : public ThreadPlan, public ThreadPlanShouldStopHere {
   }
 
 protected:
+  ThreadPlanStepOut(ThreadPlanKind kind, const char *name, Thread &thread,
+                    SymbolContext *addr_context, bool first_insn,
+                    bool stop_others, Vote report_stop_vote,
+                    Vote report_run_vote, uint32_t frame_idx,
+                    LazyBool step_out_avoids_code_without_debug_info,
+                    bool continue_to_next_branch, bool gather_return_value);
+
   void SetFlagsToDefault() override {
     GetFlags().Set(ThreadPlanStepOut::s_default_flag_values);
   }
diff --git a/lldb/include/lldb/Target/ThreadPlanStepUntil.h b/lldb/include/lldb/Target/ThreadPlanStepUntil.h
index 82f05a50b7c68..176ea5c331b79 100644
--- a/lldb/include/lldb/Target/ThreadPlanStepUntil.h
+++ b/lldb/include/lldb/Target/ThreadPlanStepUntil.h
@@ -10,11 +10,11 @@
 #define LLDB_TARGET_THREADPLANSTEPUNTIL_H
 
 #include "lldb/Target/Thread.h"
-#include "lldb/Target/ThreadPlan.h"
+#include "lldb/Target/ThreadPlanStepOut.h"
 
 namespace lldb_private {
 
-class ThreadPlanStepUntil : public ThreadPlan {
+class ThreadPlanStepUntil : public ThreadPlanStepOut {
 public:
   ThreadPlanStepUntil(Thread &thread, llvm::ArrayRef<lldb::addr_t> address_list,
                       bool stop_others, uint32_t frame_idx = 0);
@@ -24,30 +24,26 @@ class ThreadPlanStepUntil : public ThreadPlan {
   void GetDescription(Stream *s, lldb::DescriptionLevel level) override;
   bool ValidatePlan(Stream *error) override;
   bool ShouldStop(Event *event_ptr) override;
-  bool StopOthers() override;
-  lldb::StateType GetPlanRunState() override;
   bool WillStop() override;
   bool MischiefManaged() override;
+  bool IsPlanStale() override;
 
 protected:
   bool DoWillResume(lldb::StateType resume_state, bool current_plan) override;
   bool DoPlanExplainsStop(Event *event_ptr) override;
 
-  void AnalyzeStop();
-
 private:
+  /// Returns the site of the stop if it holds an until point.
+  lldb::BreakpointSiteSP GetUntilPointSite();
+  /// Completes the plan if frame zero is the until frame.
+  void CompleteIfInUntilFrame();
+
   StackID m_stack_id;
   lldb::addr_t m_step_from_insn;
-  lldb::break_id_t m_return_bp_id;
-  lldb::addr_t m_return_addr;
-  bool m_stepped_out;
-  bool m_should_stop;
-  bool m_ran_analyze;
-  bool m_explains_stop;
+  bool m_reached_until_point = false;
 
   typedef std::map<lldb::addr_t, lldb::break_id_t> until_collection;
   until_collection m_until_points;
-  bool m_stop_others;
 
   void Clear();
   void SetUntilPointsEnabled(bool enabled);
diff --git a/lldb/source/Target/ThreadPlanStepOut.cpp b/lldb/source/Target/ThreadPlanStepOut.cpp
index a98eb862a7e27..87211f6b1449d 100644
--- a/lldb/source/Target/ThreadPlanStepOut.cpp
+++ b/lldb/source/Target/ThreadPlanStepOut.cpp
@@ -64,8 +64,19 @@ ThreadPlanStepOut::ThreadPlanStepOut(
     Vote report_stop_vote, Vote report_run_vote, uint32_t frame_idx,
     LazyBool step_out_avoids_code_without_debug_info,
     bool continue_to_next_branch, bool gather_return_value)
-    : ThreadPlan(ThreadPlan::eKindStepOut, "Step out", thread, report_stop_vote,
-                 report_run_vote),
+    : ThreadPlanStepOut(ThreadPlan::eKindStepOut, "Step out", thread, context,
+                        first_insn, stop_others, report_stop_vote,
+                        report_run_vote, frame_idx,
+                        step_out_avoids_code_without_debug_info,
+                        continue_to_next_branch, gather_return_value) {}
+
+ThreadPlanStepOut::ThreadPlanStepOut(
+    ThreadPlanKind kind, const char *name, Thread &thread,
+    SymbolContext *context, bool first_insn, bool stop_others,
+    Vote report_stop_vote, Vote report_run_vote, uint32_t frame_idx,
+    LazyBool step_out_avoids_code_without_debug_info,
+    bool continue_to_next_branch, bool gather_return_value)
+    : ThreadPlan(kind, name, thread, report_stop_vote, report_run_vote),
       ThreadPlanShouldStopHere(this), m_step_from_insn(LLDB_INVALID_ADDRESS),
       m_return_bp_id(LLDB_INVALID_BREAK_ID),
       m_return_addr(LLDB_INVALID_ADDRESS), m_stop_others(stop_others),
diff --git a/lldb/source/Target/ThreadPlanStepUntil.cpp b/lldb/source/Target/ThreadPlanStepUntil.cpp
index dfb92abc1371e..8d68e43c38ab3 100644
--- a/lldb/source/Target/ThreadPlanStepUntil.cpp
+++ b/lldb/source/Target/ThreadPlanStepUntil.cpp
@@ -9,10 +9,7 @@
 #include "lldb/Target/ThreadPlanStepUntil.h"
 
 #include "lldb/Breakpoint/Breakpoint.h"
-#include "lldb/Symbol/SymbolContextScope.h"
 #include "lldb/Target/Process.h"
-#include "lldb/Target/RegisterContext.h"
-#include "lldb/Target/StopInfo.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
@@ -26,41 +23,23 @@ using namespace lldb_private;
 ThreadPlanStepUntil::ThreadPlanStepUntil(Thread &thread,
                                          llvm::ArrayRef<addr_t> address_list,
                                          bool stop_others, uint32_t frame_idx)
-    : ThreadPlan(ThreadPlan::eKindStepUntil, "Step until", thread,
-                 eVoteNoOpinion, eVoteNoOpinion),
-      m_step_from_insn(LLDB_INVALID_ADDRESS),
-      m_return_bp_id(LLDB_INVALID_BREAK_ID),
-      m_return_addr(LLDB_INVALID_ADDRESS), m_stepped_out(false),
-      m_should_stop(false), m_ran_analyze(false), m_explains_stop(false),
-      m_until_points(), m_stop_others(stop_others) {
+    : ThreadPlanStepOut(ThreadPlan::eKindStepUntil, "Step until", thread,
+                        /*addr_context=*/nullptr, /*first_insn=*/false,
+                        stop_others, eVoteNoOpinion, eVoteNoOpinion, frame_idx,
+                        /*step_out_avoids_code_without_debug_info=*/eLazyBoolNo,
+                        /*continue_to_next_branch=*/false,
+                        /*gather_return_value=*/false),
+      m_step_from_insn(LLDB_INVALID_ADDRESS) {
+  ClearShouldStopHereCallbacks();
+
   StackFrameSP frame_sp(thread.GetStackFrameAtIndex(frame_idx));
   if (!frame_sp)
     return;
 
   m_step_from_insn = frame_sp->GetStackID().GetPC();
-  Target &target = GetTarget();
-
-  // Find the return address and set a breakpoint there:
-  // FIXME - can we do this more securely if we know first_insn?
-
-  StackFrameSP return_frame_sp(thread.GetStackFrameAtIndex(frame_idx + 1));
-  if (return_frame_sp) {
-    // TODO: add inline functionality
-    m_return_addr = return_frame_sp->GetStackID().GetPC();
-    Breakpoint *return_bp =
-        target.CreateBreakpoint(m_return_addr, true, false).get();
-
-    if (return_bp != nullptr) {
-      if (return_bp->IsHardware() && !return_bp->HasResolvedLocations())
-        m_could_not_resolve_hw_bp = true;
-      return_bp->SetThreadID(m_tid);
-      m_return_bp_id = return_bp->GetID();
-      return_bp->SetBreakpointKind("until-return-backstop");
-    }
-  }
-
   m_stack_id = frame_sp->GetStackID();
 
+  Target &target = GetTarget();
   for (addr_t address : address_list) {
     Breakpoint *until_bp = target.CreateBreakpoint(address, true, false).get();
     if (until_bp != nullptr) {
@@ -77,22 +56,16 @@ ThreadPlanStepUntil::~ThreadPlanStepUntil() { Clear(); }
 
 void ThreadPlanStepUntil::Clear() {
   Target &target = GetTarget();
-  if (m_return_bp_id != LLDB_INVALID_BREAK_ID) {
-    target.RemoveBreakpointByID(m_return_bp_id);
-    m_return_bp_id = LLDB_INVALID_BREAK_ID;
-  }
-
   for (const auto &[address, break_id] : m_until_points)
     target.RemoveBreakpointByID(break_id);
   m_until_points.clear();
-  m_could_not_resolve_hw_bp = false;
 }
 
 void ThreadPlanStepUntil::GetDescription(Stream *s,
                                          lldb::DescriptionLevel level) {
   if (level == lldb::eDescriptionLevelBrief) {
     s->PutCString("step until");
-    if (m_stepped_out)
+    if (IsPlanComplete() && !m_reached_until_point)
       s->PutCString(" - stepped out");
     return;
   }
@@ -109,160 +82,60 @@ void ThreadPlanStepUntil::GetDescription(Stream *s,
     for (const auto &[address, break_id] : m_until_points)
       s->Printf("\n\t0x%" PRIx64 " (bp: %d)", (uint64_t)address, break_id);
   }
-  s->Printf(" stepped out address is 0x%" PRIx64 ".", (uint64_t)m_return_addr);
+  s->PutCString("\n");
+  ThreadPlanStepOut::GetDescription(s, level);
 }
 
 bool ThreadPlanStepUntil::ValidatePlan(Stream *error) {
-  if (m_could_not_resolve_hw_bp) {
-    if (error)
-      error->PutCString(
-          "Could not create hardware breakpoint for thread plan.");
-    return false;
-  }
-  if (m_return_bp_id == LLDB_INVALID_BREAK_ID) {
-    if (error)
-      error->PutCString("Could not create return breakpoint.");
-    return false;
-  }
   for (const auto &[address, break_id] : m_until_points)
     if (!LLDB_BREAK_ID_IS_VALID(break_id))
       return false;
-  return true;
+  return ThreadPlanStepOut::ValidatePlan(error);
 }
 
-void ThreadPlanStepUntil::AnalyzeStop() {
-  if (m_ran_analyze)
-    return;
-
+BreakpointSiteSP ThreadPlanStepUntil::GetUntilPointSite() {
   StopInfoSP stop_info_sp = GetPrivateStopInfo();
-  m_should_stop = true;
-  m_explains_stop = false;
-
-  if (stop_info_sp) {
-    StopReason reason = stop_info_sp->GetStopReason();
-
-    if (reason == eStopReasonBreakpoint) {
-      // If this is OUR breakpoint, we're fine, otherwise we don't know why
-      // this happened...
-      BreakpointSiteSP this_site =
-          m_process.GetBreakpointSiteList().FindByID(stop_info_sp->GetValue());
-      if (!this_site) {
-        m_explains_stop = false;
-        return;
-      }
-
-      if (this_site->IsBreakpointAtThisSite(m_return_bp_id)) {
-        // If we are at our "step out" breakpoint, and the stack depth has
-        // shrunk, then this is indeed our stop. If the stack depth has grown,
-        // then we've hit our step out breakpoint recursively. If we are the
-        // only breakpoint at that location, then we do explain the stop, and
-        // we'll just continue. If there was another breakpoint here, then we
-        // don't explain the stop, but we won't mark ourselves Completed,
-        // because maybe that breakpoint will continue, and then we'll finish
-        // the "until".
-        bool done;
-        StackID cur_frame_zero_id;
-
-        done = (m_stack_id.IsYoungerThan(cur_frame_zero_id));
-
-        if (done) {
-          m_stepped_out = true;
-          SetPlanComplete();
-        } else
-          m_should_stop = false;
-
-        if (this_site->GetNumberOfConstituents() == 1)
-          m_explains_stop = true;
-        else
-          m_explains_stop = false;
-        return;
-      } else {
-        // Check if we've hit one of our "until" breakpoints.
-        until_collection::iterator pos, end = m_until_points.end();
-        for (pos = m_until_points.begin(); pos != end; pos++) {
-          if (this_site->IsBreakpointAtThisSite((*pos).second)) {
-            // If we're at the right stack depth, then we're done.
-            Thread &thread = GetThread();
-            bool done;
-            StackID frame_zero_id =
-                thread.GetStackFrameAtIndex(0)->GetStackID();
+  if (!stop_info_sp || stop_info_sp->GetStopReason() != eStopReasonBreakpoint)
+    return nullptr;
 
-            if (frame_zero_id == m_stack_id)
-              done = true;
-            else if (frame_zero_id.IsYoungerThan(m_stack_id))
-              done = false;
-            else {
-              StackFrameSP older_frame_sp = thread.GetStackFrameAtIndex(1);
+  BreakpointSiteSP site_sp =
+      m_process.GetBreakpointSiteList().FindByID(stop_info_sp->GetValue());
+  if (!site_sp)
+    return nullptr;
 
-              // But if we can't even unwind one frame we should just get out
-              // of here & stop...
-              if (older_frame_sp) {
-                const SymbolContext &older_context =
-                    older_frame_sp->GetSymbolContext(eSymbolContextEverything);
-                SymbolContext stack_context;
-                m_stack_id.GetSymbolContextScope()->CalculateSymbolContext(
-                    &stack_context);
-
-                done = (older_context == stack_context);
-              } else
-                done = false;
-            }
-
-            if (done)
-              SetPlanComplete();
-            else
-              m_should_stop = false;
+  for (const auto &[address, break_id] : m_until_points)
+    if (site_sp->IsBreakpointAtThisSite(break_id))
+      return site_sp;
+  return nullptr;
+}
 
-            // Otherwise we've hit this breakpoint recursively.  If we're the
-            // only breakpoint here, then we do explain the stop, and we'll
-            // continue. If not then we should let higher plans handle this
-            // stop.
-            if (this_site->GetNumberOfConstituents() == 1)
-              m_explains_stop = true;
-            else {
-              m_should_stop = true;
-              m_explains_stop = false;
-            }
-            return;
-          }
-        }
-      }
-      // If we get here we haven't hit any of our breakpoints, so let the
-      // higher plans take care of the stop.
-      m_explains_stop = false;
-      return;
-    } else if (IsUsuallyUnexplainedStopReason(reason)) {
-      m_explains_stop = false;
-    } else {
-      m_explains_stop = true;
-    }
-  }
+void ThreadPlanStepUntil::CompleteIfInUntilFrame() {
+  StackID frame_zero_id = GetThread().GetStackFrameAtIndex(0)->GetStackID();
+  // Hits in younger frames, e.g. recursive calls, do not count.
+  if (frame_zero_id.IsYoungerThan(m_stack_id))
+    return;
+  m_reached_until_point = true;
+  SetPlanComplete();
 }
 
 bool ThreadPlanStepUntil::DoPlanExplainsStop(Event *event_ptr) {
-  // We don't explain signals or breakpoints (breakpoints that handle stepping
-  // in or out will be handled by a child plan.
-  AnalyzeStop();
-  return m_explains_stop;
+  BreakpointSiteSP site_sp = GetUntilPointSite();
+  if (!site_sp)
+    return ThreadPlanStepOut::DoPlanExplainsStop(event_ptr);
+
+  CompleteIfInUntilFrame();
+  // A user breakpoint at the same site takes precedence in the stop report.
+  return !site_sp->ContainsUserBreakpointForThread(GetThread());
 }
 
 bool ThreadPlanStepUntil::ShouldStop(Event *event_ptr) {
-  // If we've told our self in ExplainsStop that we plan to continue, then do
-  // so here.  Otherwise, as long as this thread has stopped for a reason, we
-  // will stop.
-
-  StopInfoSP stop_info_sp = GetPrivateStopInfo();
-  if (!stop_info_sp || stop_info_sp->GetStopReason() == eStopReasonNone)
-    return false;
-
-  AnalyzeStop();
-  return m_should_stop;
+  if (GetUntilPointSite()) {
+    CompleteIfInUntilFrame();
+    return IsPlanComplete();
+  }
+  return ThreadPlanStepOut::ShouldStop(event_ptr);
 }
 
-bool ThreadPlanStepUntil::StopOthers() { return m_stop_others; }
-
-StateType ThreadPlanStepUntil::GetPlanRunState() { return eStateRunning; }
-
 void ThreadPlanStepUntil::SetUntilPointsEnabled(bool enabled) {
   Target &target = GetTarget();
   for (const auto &[address, break_id] : m_until_points)
@@ -272,35 +145,26 @@ void ThreadPlanStepUntil::SetUntilPointsEnabled(bool enabled) {
 
 bool ThreadPlanStepUntil::DoWillResume(StateType resume_state,
                                        bool current_plan) {
-  if (current_plan) {
-    Target &target = GetTarget();
-    Breakpoint *return_bp = target.GetBreakpointByID(m_return_bp_id).get();
-    if (return_bp != nullptr)
-      return_bp->SetEnabled(true);
-    SetUntilPointsEnabled(true);
-  }
-
-  m_should_stop = true;
-  m_ran_analyze = false;
-  m_explains_stop = false;
-  return true;
+  SetUntilPointsEnabled(true);
+  return ThreadPlanStepOut::DoWillResume(resume_state, current_plan);
 }
 
 bool ThreadPlanStepUntil::WillStop() {
-  Target &target = GetTarget();
-  Breakpoint *return_bp = target.GetBreakpointByID(m_return_bp_id).get();
-  if (return_bp != nullptr)
-    return_bp->SetEnabled(false);
   SetUntilPointsEnabled(false);
-  return true;
+  return ThreadPlanStepOut::WillStop();
+}
+
+bool ThreadPlanStepUntil::IsPlanStale() {
+  return IsPlanComplete() || ThreadPlanStepOut::IsPlanStale();
 }
 
 bool ThreadPlanStepUntil::MischiefManaged() {
-  if (!IsPlanComplete())
+  // StepOut::MischiefManaged returns true if and only the plan is completed, so
+  // it also checks StepUntil's status.
+  if (!ThreadPlanStepOut::MischiefManaged())
     return false;
 
   LLDB_LOGF(GetLog(LLDBLog::Step), "Completed step until plan.");
   Clear();
-  ThreadPlan::MischiefManaged();
   return true;
 }
diff --git a/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py b/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
index 2188614b4fcda..a9e2ab1c7af63 100644
--- a/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
+++ b/lldb/test/API/functionalities/inline-frame-pc/TestInlineFrameUntil.py
@@ -39,6 +39,25 @@ def test_until_in_same_inline_frame(self):
     def test_until_in_parent_inline_frame(self):
         self.check_stops_at(2, "// until in level2")
 
+    # If an inlined frame is frame zero and thread until targets a "continue"
+    # line, there are two breakpoints there: one for the until breakpoint, one
+    # for the step over range. The step over range explains the stop first, so
+    # the until plan misses it.
+    # def test_until_jump_in_inline_frame(self):
+    #     self.check_stops_at(1, "// until jump in level3")
+
+    def test_until_target_already_ran(self):
+        frame = self.until_from_frame(1, "// before sink")
+        self.assertEqual(frame.GetFunctionName(), "level2")
+
+    def test_until_return_address_in_inline_frame(self):
+        thread = self.run_to_sink()
+        return_address = thread.GetFrameAtIndex(1).GetPC()
+        self.runCmd(f"thread until -f 1 -a {return_address}")
+        frame = thread.GetFrameAtIndex(0)
+        self.assertEqual(frame.GetFunctionName(), "level3")
+        self.assertEqual(frame.GetPC(), return_address)
+
     def test_until_in_inlining_caller(self):
         self.check_rejected(1, "// until in level2")
 
diff --git a/lldb/test/API/functionalities/inline-frame-pc/main.c b/lldb/test/API/functionalities/inline-frame-pc/main.c
index 2ab720a609f13..bc0daf22d8cc2 100644
--- a/lldb/test/API/functionalities/inline-frame-pc/main.c
+++ b/lldb/test/API/functionalities/inline-frame-pc/main.c
@@ -6,6 +6,10 @@ static inline __attribute__((always_inline)) void level3(int a) {
   g = a; // before sink
   sink(a);
   g = a + 1; // break in level3
+  while (g < a + 3) {
+    g++;
+    continue; // until jump in level3
+  }
   g = a + 2; // until in level3
 }
 
diff --git a/lldb/test/API/functionalities/thread/step_until_recursive/Makefile b/lldb/test/API/functionalities/thread/step_until_recursive/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/step_until_recursive/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/thread/step_until_recursive/TestStepUntilRecursive.py b/lldb/test/API/functionalities/thread/step_until_recursive/TestStepUntilRecursive.py
new file mode 100644
index 0000000000000..63d784de63197
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/step_until_recursive/TestStepUntilRecursive.py
@@ -0,0 +1,27 @@
+"""
+Test thread until in a recursive function.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestStepUntilRecursive(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_until_after_recursive_call(self):
+        self.build()
+        _, _, thread, _ = lldbutil.run_to_source_breakpoint(
+            self, "// base case", lldb.SBFileSpec("main.c")
+        )
+        after_call = line_number("main.c", "// after recursive call")
+        self.assertSuccess(
+            thread.StepOverUntil(
+                thread.GetFrameAtIndex(1), lldb.SBFileSpec("main.c"), after_call
+            )
+        )
+        frame = thread.GetFrameAtIndex(0)
+        self.assertEqual(frame.GetLineEntry().GetLine(), after_call)
+        self.assertEqual(frame.FindVariable("n").GetValueAsSigned(), 1)
diff --git a/lldb/test/API/functionalities/thread/step_until_recursive/main.c b/lldb/test/API/functionalities/thread/step_until_recursive/main.c
new file mode 100644
index 0000000000000..03466b684ecb6
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/step_until_recursive/main.c
@@ -0,0 +1,14 @@
+volatile int g;
+
+__attribute__((noinline)) void rec(int n) {
+  if (n > 0)
+    rec(n - 1); // recursive call
+  else
+    g = 0; // base case
+  g = n; // after recursive call
+}
+
+int main(void) {
+  rec(2);
+  return 0;
+}



More information about the lldb-commits mailing list