Repository: swiftlang/llvm-project
Pull request #10576: [lldb][swift] Create thread plan for stepping through Allocating Init
Author: login: felipepiovezan
Created: 2025-04-29T02:04:01Z | Merged: 2025-05-13T15:48:12Z

These trampolines have a well defined target, which can be extracted
from the demangle tree. A similar strategy is already adopted for
stepping into conformance code. This allows LLDB to create a
RunToAddress plan with a target address, which is obtained by searching
the Module for functions matching `<class_name>.Init`.

We already had a test for this, but we rely on the "step avoid
libraries" setting to emulate the case of "avoid libraries without debug
info". However, they are not exactly equivalent, so the test mislead us
into believing stepping was working. A future patch should try to fold
the setting-checking code into
ThreadPlanShouldStopHere::DefaultShouldStopCallback.

With this patch, the test now works regardless of whether debug info is
present or not.

To make this work, this commit also teaches the Swift language runtime
how to recognize "type metadata accessors". These are thunks in the
sense that they jump to some other code, but not user code. Their
purposes is to eventually send the "self" message to the SwiftObject the
first time they run. As such, they should just be stepped out from,
which is accomplished by having the runtime recognize them as
trampolines but not provide any specific actions, relying on the default
behavior of ShouldStopHere.

Prior to this change, this would sometimes work accidentally because all
the functions involved had line 0 debug information, and the debugger
would keep stepping through _all_ of them. However, in debug builds of
x86, this strategy failed; sometimes the computed Symbol size is larger
than the line entry for these functions, and so the detection "all line
0" fails. Relying on this is error-prone and slow, as it causes the
debugger to stop many times.


Diff:
diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
index 98a9c1dfd2c9a3..bfd20462717d18 100644
--- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
@@ -53,6 +53,7 @@ enum class ThunkAction {
   Unknown = 0,
   GetThunkTarget,
   StepIntoConformance,
+  StepIntoAllocatingInit,
   StepThrough,
 };
 
@@ -336,7 +337,7 @@ static const char *GetThunkKindName(ThunkKind kind) {
   case ThunkKind::Unknown:
     return "Unknown";
   case ThunkKind::AllocatingInit:
-    return "StepThrough";
+    return "StepIntoAllocatingInit";
   case ThunkKind::PartialApply:
     return "GetThunkTarget";
   case ThunkKind::ObjCAttribute:
@@ -353,7 +354,7 @@ static ThunkAction GetThunkAction(ThunkKind kind) {
   case ThunkKind::Unknown:
     return ThunkAction::Unknown;
   case ThunkKind::AllocatingInit:
-    return ThunkAction::StepThrough;
+    return ThunkAction::StepIntoAllocatingInit;
   case ThunkKind::PartialApply:
     return ThunkAction::GetThunkTarget;
   case ThunkKind::ObjCAttribute:
@@ -589,18 +590,50 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
         thread, sym_addr_range, sc, function_name.c_str(), eOnlyDuringStepping,
         eLazyBoolNo, eLazyBoolNo);
   }
+  case ThunkAction::StepIntoAllocatingInit: {
+    LLDB_LOGF(log, "Stepping into allocating init: \"%s\"", symbol_name);
+    swift::Demangle::Context ctx;
+    NodePointer demangled_node =
+        SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx);
+
+    using Kind = Node::Kind;
+    NodePointer class_node = childAtPath(
+        demangled_node, {Kind::Allocator, Kind::Class, Kind::Identifier});
+    if (!class_node || !class_node->hasText()) {
+      std::string node_str = getNodeTreeAsString(demangled_node);
+      LLDB_LOGF(log,
+                "Failed to extract constructor name from demangle node: %s",
+                node_str.c_str());
+      return nullptr;
+    }
+
+    ModuleFunctionSearchOptions options{/*include_symbols*/ true,
+                                        /*include_inlines*/ true};
+    std::string ctor_name = llvm::formatv("{0}.init", class_node->getText());
+    SymbolContextList sc_list;
+    sc.module_sp->FindFunctions(RegularExpression(ctor_name), options, sc_list);
+    std::vector<addr_t> load_addresses;
+    Target &target = thread.GetProcess()->GetTarget();
+    for (const SymbolContext &ctor_sc : sc_list) {
+      const Symbol *ctor_symbol = ctor_sc.symbol;
+      if (ctor_symbol)
+        load_addresses.push_back(ctor_symbol->GetLoadAddress(&target));
+    }
+    if (load_addresses.empty())
+      return nullptr;
+    return std::make_shared<ThreadPlanRunToAddress>(thread, load_addresses,
+                                                    stop_others);
+  }
   case ThunkAction::StepThrough: {
     if (log)
       log->Printf("Stepping through thunk: %s kind: %s", symbol_name,
                   GetThunkKindName(thunk_kind));
     AddressRange sym_addr_range(sc.symbol->GetAddress(),
                                 sc.symbol->GetByteSize());
-    ThreadPlanSP new_plan_sp = std::make_shared<ThreadPlanStepInRange>(thread, sym_addr_range, sc,
-                                                   nullptr, eOnlyDuringStepping,
-                                                   eLazyBoolNo, eLazyBoolNo);
-    static_cast<ThreadPlanStepInRange *>(new_plan_sp.get())
-        ->GetFlags().Clear(ThreadPlanShouldStopHere::eStepOutPastThunks);
-    return new_plan_sp;     
+    ThreadPlanSP new_plan_sp = std::make_shared<ThreadPlanStepInRange>(
+        thread, sym_addr_range, sc, nullptr, eOnlyDuringStepping, eLazyBoolNo,
+        eLazyBoolNo);
+    return new_plan_sp;
   }
   }
 
@@ -613,7 +646,15 @@ bool SwiftLanguageRuntime::IsSymbolARuntimeThunk(const Symbol &symbol) {
   if (symbol_name.empty())
     return false;
   swift::Demangle::Context demangle_ctx;
-  return demangle_ctx.isThunkSymbol(symbol_name);
+  if (demangle_ctx.isThunkSymbol(symbol_name))
+    return true;
+
+  // These are not Thunks in the sense that they don't jump to *user* code.
+  // But they are language symbols that should be identified as something to act
+  // on; here, the default action of stepping out is appropriate.
+  Node *node = demangle_ctx.demangleSymbolAsNode(symbol_name);
+  return node && node->getKind() == Node::Kind::Global &&
+         hasChild(node, Node::Kind::TypeMetadataAccessFunction);
 }
 
 bool SwiftLanguageRuntime::IsSwiftMangledName(llvm::StringRef name) {
diff --git a/lldb/test/API/lang/swift/cxx_interop/backward/stepping/TestSwiftBackwardInteropStepping.py b/lldb/test/API/lang/swift/cxx_interop/backward/stepping/TestSwiftBackwardInteropStepping.py
index 29f4eed6269af0..8ebc198781d9ec 100644
--- a/lldb/test/API/lang/swift/cxx_interop/backward/stepping/TestSwiftBackwardInteropStepping.py
+++ b/lldb/test/API/lang/swift/cxx_interop/backward/stepping/TestSwiftBackwardInteropStepping.py
@@ -53,7 +53,6 @@ def test_method_step_over_class(self):
         thread = self.setup('Break here for method - class')
         self.check_step_over(thread, 'testMethod')
 
-    @expectedFailureAll(bugnumber="rdar://106670255")
     @swiftTest
     def test_init_step_in_class(self):
         thread = self.setup('Break here for constructor - class')
@@ -115,7 +114,6 @@ def test_method_step_over_struct_class(self):
         thread = self.setup('Break here for method - struct')
         self.check_step_over(thread, 'testMethod')
 
-    @expectedFailureAll(bugnumber="rdar://106670255")
     @swiftTest
     def test_init_step_in_struct_class(self):
         thread = self.setup('Break here for constructor - struct')
diff --git a/lldb/test/API/lang/swift/step_through_allocating_init/TestStepThroughAllocatingInit.py b/lldb/test/API/lang/swift/step_through_allocating_init/TestStepThroughAllocatingInit.py
index 5911f1da164b6d..897e6bc2b114a9 100644
--- a/lldb/test/API/lang/swift/step_through_allocating_init/TestStepThroughAllocatingInit.py
+++ b/lldb/test/API/lang/swift/step_through_allocating_init/TestStepThroughAllocatingInit.py
@@ -25,18 +25,6 @@ def setUp(self):
         lldbtest.TestBase.setUp(self)
         self.main_source = "main.swift"
         self.main_source_spec = lldb.SBFileSpec(self.main_source)
-        # If you are running against a debug swift you are going to
-        # end up stepping into the stdlib and that will make stepping
-        # tests impossible to write.  So avoid that.
-
-        if platform.system() == 'Darwin':
-            lib_name = "libswiftCore.dylib"
-        else:
-            lib_name = "libswiftCore.so"
-
-        self.dbg.HandleCommand(
-            "settings set "
-            "target.process.thread.step-avoid-libraries {}".format(lib_name))
 
     def do_test(self, use_api):
         """Tests that we can step reliably in swift code."""


Reviews:
user: login: adrian-prantl
state: APPROVED
author_association: NONE
submitted_at: 2025-05-08T00:11:16Z
user: login: jimingham
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T22:44:08Z
user: login: felipepiovezan
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T22:48:54Z
user: login: jimingham
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T23:00:19Z
user: login: jimingham
body: LGTM
state: APPROVED
author_association: NONE
submitted_at: 2025-05-12T23:00:34Z

Review comments:
diff_hunk: @@ -589,18 +590,50 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
         thread, sym_addr_range, sc, function_name.c_str(), eOnlyDuringStepping,
         eLazyBoolNo, eLazyBoolNo);
   }
+  case ThunkAction::StepIntoAllocatingInit: {
+    LLDB_LOGF(log, "Stepping into allocating init: \"%s\"", symbol_name);
+    swift::Demangle::Context ctx;
+    NodePointer demangled_node =
+        SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx);
+
+    using Kind = Node::Kind;
+    NodePointer class_node = childAtPath(
path: lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
user: login: jimingham
body: Do you need to check that demangled_node is non-null before calling childAtPath?
created_at: 2025-05-12T22:44:08Z
author_association: NONE
diff_hunk: @@ -589,18 +590,50 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
         thread, sym_addr_range, sc, function_name.c_str(), eOnlyDuringStepping,
         eLazyBoolNo, eLazyBoolNo);
   }
+  case ThunkAction::StepIntoAllocatingInit: {
+    LLDB_LOGF(log, "Stepping into allocating init: \"%s\"", symbol_name);
+    swift::Demangle::Context ctx;
+    NodePointer demangled_node =
+        SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx);
+
+    using Kind = Node::Kind;
+    NodePointer class_node = childAtPath(
path: lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
user: login: felipepiovezan
body: Oh, the function `childAtPath` is safe, it checks for nullptr. (If it didn't, we would have changed it to take a reference instead)
created_at: 2025-05-12T22:48:54Z
author_association: NONE
diff_hunk: @@ -589,18 +590,50 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
         thread, sym_addr_range, sc, function_name.c_str(), eOnlyDuringStepping,
         eLazyBoolNo, eLazyBoolNo);
   }
+  case ThunkAction::StepIntoAllocatingInit: {
+    LLDB_LOGF(log, "Stepping into allocating init: \"%s\"", symbol_name);
+    swift::Demangle::Context ctx;
+    NodePointer demangled_node =
+        SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx);
+
+    using Kind = Node::Kind;
+    NodePointer class_node = childAtPath(
path: lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp
user: login: jimingham
body: Okay.
created_at: 2025-05-12T23:00:19Z
author_association: NONE

PR comments:
user: login: felipepiovezan
created_at: 2025-04-29T02:04:23Z
body: @swift-ci test
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-02T21:46:25Z
body: @swift-ci test macos platform

author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-07T22:08:33Z
body: @swift-ci test macos platform
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-09T19:30:16Z
body: @swift-ci test
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-09T19:31:42Z
body: @swift-ci test
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-12T22:19:45Z
body: @swift-ci test
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-12T22:21:59Z
body: After discussing this with Jim, I changed the implementation to use a much more targeted plan: run-to-address instead of stepping-in-to-target.
author_association: NONE
user: login: felipepiovezan
created_at: 2025-05-12T22:22:04Z
body: @swift-ci test
author_association: NONE