Skip to content

Commit 8ab9cf4

Browse files
[lldb] Implement swift diagnostic for non-captured vars in "frame var"
This commit implements SwiftLanguage::GetParentNameIfClosure, effectively adding support for diagnosing when a `frame var` command inside a closure targets a variable that was not captured.
1 parent 043a2de commit 8ab9cf4

File tree

8 files changed

+256
-0
lines changed

8 files changed

+256
-0
lines changed

lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,11 @@ SwiftLanguage::AreEqualForFrameComparison(const SymbolContext &sc1,
20012001
llvm_unreachable("unhandled enumeration in AreEquivalentFunctions");
20022002
}
20032003

2004+
std::string
2005+
SwiftLanguage::GetParentNameIfClosure(llvm::StringRef mangled_name) const {
2006+
return SwiftLanguageRuntime::GetParentNameIfClosure(mangled_name);
2007+
}
2008+
20042009
//------------------------------------------------------------------
20052010
// Static Functions
20062011
//------------------------------------------------------------------

lldb/source/Plugins/Language/Swift/SwiftLanguage.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class SwiftLanguage : public Language {
8181
std::optional<bool>
8282
AreEqualForFrameComparison(const SymbolContext &sc1,
8383
const SymbolContext &sc2) const override;
84+
85+
std::string
86+
GetParentNameIfClosure(llvm::StringRef mangled_name) const override;
8487
//------------------------------------------------------------------
8588
// Static Functions
8689
//------------------------------------------------------------------

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ class SwiftLanguageRuntime : public LanguageRuntime {
205205
const SymbolContext *sc = nullptr,
206206
const ExecutionContext *exe_ctx = nullptr);
207207

208+
static std::string GetParentNameIfClosure(llvm::StringRef mangled_name);
209+
208210
/// Demangle a symbol to a swift::Demangle node tree.
209211
///
210212
/// This is a central point of access, for purposes such as logging.

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
#include "Plugins/TypeSystem/Swift/SwiftASTContext.h"
14+
#include "Plugins/TypeSystem/Swift/SwiftDemangle.h"
1315
#include "SwiftLanguageRuntime.h"
1416

1517
#include "lldb/Breakpoint/StoppointCallbackContext.h"
@@ -1496,4 +1498,30 @@ SwiftLanguageRuntime::GetGenericSignature(llvm::StringRef function_name,
14961498
return signature;
14971499
}
14981500

1501+
std::string SwiftLanguageRuntime::GetParentNameIfClosure(StringRef name) {
1502+
using Kind = Node::Kind;
1503+
swift::Demangle::Context ctx;
1504+
auto *node = SwiftLanguageRuntime::DemangleSymbolAsNode(name, ctx);
1505+
if (!node || node->getKind() != Node::Kind::Global)
1506+
return "";
1507+
1508+
// Replace the top level closure node with the child function-like node, and
1509+
// attempt to remangle. If successful, this produces the parent function-like
1510+
// entity.
1511+
static const auto closure_kinds = {Kind::ImplicitClosure,
1512+
Kind::ExplicitClosure};
1513+
static const auto function_kinds = {Kind::ImplicitClosure,
1514+
Kind::ExplicitClosure, Kind::Function};
1515+
auto *closure_node = swift_demangle::GetFirstChildOfKind(node, closure_kinds);
1516+
auto *parent_func_node =
1517+
swift_demangle::GetFirstChildOfKind(closure_node, function_kinds);
1518+
if (!parent_func_node)
1519+
return "";
1520+
swift_demangle::ReplaceChildWith(*node, *closure_node, *parent_func_node);
1521+
1522+
if (ManglingErrorOr<std::string> mangled = swift::Demangle::mangleNode(node);
1523+
mangled.isSuccess())
1524+
return mangled.result();
1525+
return "";
1526+
}
14991527
} // namespace lldb_private

lldb/source/Plugins/TypeSystem/Swift/SwiftDemangle.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,32 @@
2222
namespace lldb_private {
2323
namespace swift_demangle {
2424

25+
using NodePointer = swift::Demangle::NodePointer;
26+
using Node = swift::Demangle::Node;
27+
28+
/// Returns the first child of `node` whose kind is in `kinds`.
29+
inline NodePointer GetFirstChildOfKind(NodePointer node,
30+
llvm::ArrayRef<Node::Kind> kinds) {
31+
if (!node)
32+
return nullptr;
33+
for (auto *child : *node)
34+
if (llvm::is_contained(kinds, child->getKind()))
35+
return child;
36+
return nullptr;
37+
}
38+
39+
/// Assumes that `to_replace` is a child of `parent`, and replaces it with
40+
/// `new_child`. The Nodes must all be owned by the same context.
41+
inline void ReplaceChildWith(Node &parent, Node &to_replace, Node &new_child) {
42+
for (unsigned idx = 0; idx < parent.getNumChildren(); idx++) {
43+
auto *child = parent.getChild(idx);
44+
if (child == &to_replace)
45+
parent.replaceChild(idx, &new_child);
46+
return;
47+
}
48+
llvm_unreachable("invalid child passed to replaceChildWith");
49+
}
50+
2551
/// Access an inner node by following the given Node::Kind path.
2652
///
2753
/// Note: The Node::Kind path is relative to the given root node. The root
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SWIFT_SOURCES := main.swift
2+
3+
include Makefile.rules
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Test that we can print and call closures passed in various contexts
3+
"""
4+
5+
import os
6+
import re
7+
import lldb
8+
import lldbsuite.test.lldbutil as lldbutil
9+
from lldbsuite.test.decorators import *
10+
from lldbsuite.test.lldbtest import *
11+
12+
13+
def check_not_captured_error(test, frame, var_name, parent_function):
14+
expected_error = (
15+
f"A variable named '{var_name}' existed in function '{parent_function}'"
16+
)
17+
test.expect(f"frame variable {var_name}", substrs=[expected_error], error=True)
18+
19+
20+
def check_no_enhanced_diagnostic(test, frame, var_name):
21+
forbidden_str = "A variable named"
22+
value = frame.EvaluateExpression(var_name)
23+
error = value.GetError().GetCString()
24+
test.assertNotIn(forbidden_str, error)
25+
26+
value = frame.EvaluateExpression(f"1 + {var_name} + 1")
27+
error = value.GetError().GetCString()
28+
test.assertNotIn(forbidden_str, error)
29+
30+
test.expect(
31+
f"frame variable {var_name}",
32+
substrs=[forbidden_str],
33+
matching=False,
34+
error=True,
35+
)
36+
37+
38+
class TestSwiftClosureVarNotCaptured(TestBase):
39+
def get_to_bkpt(self, bkpt_name):
40+
return lldbutil.run_to_source_breakpoint(
41+
self, bkpt_name, lldb.SBFileSpec("main.swift")
42+
)
43+
44+
@swiftTest
45+
def test_simple_closure(self):
46+
self.build()
47+
(target, process, thread, bkpt) = self.get_to_bkpt("break_simple_closure")
48+
check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_1(arg:)")
49+
check_not_captured_error(self, thread.frames[0], "arg", "func_1(arg:)")
50+
check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me")
51+
52+
@swiftTest
53+
def test_nested_closure(self):
54+
self.build()
55+
(target, process, thread, bkpt) = self.get_to_bkpt("break_double_closure_1")
56+
check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_2(arg:)")
57+
check_not_captured_error(self, thread.frames[0], "arg", "func_2(arg:)")
58+
check_not_captured_error(
59+
self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_2(arg:)"
60+
)
61+
check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me")
62+
63+
lldbutil.continue_to_source_breakpoint(
64+
self, process, "break_double_closure_2", lldb.SBFileSpec("main.swift")
65+
)
66+
check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_2(arg:)")
67+
check_not_captured_error(self, thread.frames[0], "arg", "func_2(arg:)")
68+
check_not_captured_error(
69+
self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_2(arg:)"
70+
)
71+
check_not_captured_error(
72+
self, thread.frames[0], "shadowed_var", "closure #1 in func_2(arg:)"
73+
)
74+
check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me")
75+
76+
@swiftTest
77+
# Async variable inspection on Linux/Windows are still problematic.
78+
@skipIf(oslist=["windows", "linux"])
79+
def test_async_closure(self):
80+
self.build()
81+
(target, process, thread, bkpt) = self.get_to_bkpt("break_async_closure_1")
82+
check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_3(arg:)")
83+
check_not_captured_error(self, thread.frames[0], "arg", "func_3(arg:)")
84+
check_not_captured_error(
85+
self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_3(arg:)"
86+
)
87+
check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me")
88+
89+
lldbutil.continue_to_source_breakpoint(
90+
self, process, "break_async_closure_2", lldb.SBFileSpec("main.swift")
91+
)
92+
check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_3(arg:)")
93+
check_not_captured_error(self, thread.frames[0], "arg", "func_3(arg:)")
94+
check_not_captured_error(
95+
self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_3(arg:)"
96+
)
97+
check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
func func_1(arg: Int) {
2+
let var_in_foo = "Alice"
3+
do {
4+
let dont_find_me = "haha"
5+
print(dont_find_me)
6+
}
7+
let simple_closure = {
8+
print("Hi there!") // break_simple_closure
9+
}
10+
simple_closure()
11+
let dont_find_me = "haha"
12+
print(dont_find_me)
13+
}
14+
15+
func func_2(arg: Int) {
16+
do {
17+
let dont_find_me = "haha"
18+
print(dont_find_me)
19+
}
20+
let var_in_foo = "Alice"
21+
let shadowed_var = "shadow"
22+
let outer_closure = {
23+
do {
24+
let dont_find_me = "haha"
25+
print(dont_find_me)
26+
}
27+
let var_in_outer_closure = "Alice"
28+
let shadowed_var = "shadow2"
29+
let inner_closure_1 = {
30+
print("Hi inside!") // break_double_closure_1
31+
}
32+
inner_closure_1()
33+
34+
do {
35+
let dont_find_me = "haha"
36+
print(dont_find_me)
37+
}
38+
let inner_closure_2 = {
39+
print("Hi inside!") // break_double_closure_2
40+
}
41+
inner_closure_2()
42+
let dont_find_me = "haha"
43+
print(dont_find_me)
44+
}
45+
let dont_find_me = "haha"
46+
print(dont_find_me)
47+
outer_closure()
48+
}
49+
50+
func func_3(arg: Int) async {
51+
do {
52+
let dont_find_me = "haha"
53+
print(dont_find_me)
54+
}
55+
let var_in_foo = "Alice"
56+
57+
// FIXME: if we comment the line below, the test fails. For some reason,
58+
// without this line, most variables don't have debug info in the entry
59+
// funclet, which is the "parent name" derived from the closure name.
60+
// rdar://152271048
61+
try! await Task.sleep(for: .seconds(0))
62+
63+
let outer_closure = {
64+
do {
65+
let dont_find_me = "haha"
66+
print(dont_find_me)
67+
}
68+
let var_in_outer_closure = "Alice"
69+
70+
let inner_closure_1 = {
71+
print("Hi inside!") // break_async_closure_1
72+
}
73+
inner_closure_1()
74+
75+
try await Task.sleep(for: .seconds(0))
76+
77+
let inner_closure_2 = {
78+
print("Hi inside!") // break_async_closure_2
79+
}
80+
inner_closure_2()
81+
let dont_find_me = "haha"
82+
print(dont_find_me)
83+
}
84+
85+
try! await outer_closure()
86+
let dont_find_me = "haha"
87+
print(dont_find_me)
88+
}
89+
90+
func_1(arg: 42)
91+
func_2(arg: 42)
92+
await func_3(arg: 42)

0 commit comments

Comments
 (0)