diff --git a/lldb/include/lldb/Expression/DiagnosticManager.h b/lldb/include/lldb/Expression/DiagnosticManager.h index cc802b6f48334..fc49349d1b7c3 100644 --- a/lldb/include/lldb/Expression/DiagnosticManager.h +++ b/lldb/include/lldb/Expression/DiagnosticManager.h @@ -107,7 +107,8 @@ class DiagnosticManager { m_fixed_expression.clear(); } - const DiagnosticList &Diagnostics() { return m_diagnostics; } + const DiagnosticList &Diagnostics() const { return m_diagnostics; } + DiagnosticList &Diagnostics() { return m_diagnostics; } bool HasFixIts() const { return llvm::any_of(m_diagnostics, diff --git a/lldb/include/lldb/Symbol/Variable.h b/lldb/include/lldb/Symbol/Variable.h index 4504fc1f5f45f..64bf69f25eda2 100644 --- a/lldb/include/lldb/Symbol/Variable.h +++ b/lldb/include/lldb/Symbol/Variable.h @@ -91,6 +91,9 @@ class Variable : public UserID, public std::enable_shared_from_this { bool IsInScope(StackFrame *frame); + /// Returns true if this variable is in scope at `addr` inside `block`. + bool IsInScope(const Block &block, const Address &addr); + bool LocationIsValidForFrame(StackFrame *frame); bool LocationIsValidForAddress(const Address &address); diff --git a/lldb/include/lldb/Symbol/VariableList.h b/lldb/include/lldb/Symbol/VariableList.h index fd15f3ae6891f..ead300a815cb9 100644 --- a/lldb/include/lldb/Symbol/VariableList.h +++ b/lldb/include/lldb/Symbol/VariableList.h @@ -24,6 +24,9 @@ class VariableList { VariableList(); virtual ~VariableList(); + VariableList(VariableList &&) = default; + VariableList &operator=(VariableList &&) = default; + void AddVariable(const lldb::VariableSP &var_sp); bool AddVariableIfUnique(const lldb::VariableSP &var_sp); diff --git a/lldb/include/lldb/Target/Language.h b/lldb/include/lldb/Target/Language.h index 9533f9c99b0ea..4403826da1c81 100644 --- a/lldb/include/lldb/Target/Language.h +++ b/lldb/include/lldb/Target/Language.h @@ -403,6 +403,28 @@ class Language : public PluginInterface { return nullptr; } + // BEGIN SWIFT + // Implement LanguageCPlusPlus::GetParentNameIfClosure and upstream this. + // rdar://152321823 + + /// If `mangled_name` refers to a function that is a "closure-like" function, + /// returns the name of the parent function where the input closure was + /// defined. Returns an empty string if there is no such parent, or if the + /// query does not make sense for this language. + virtual std::string + GetParentNameIfClosure(llvm::StringRef mangled_name) const { + return ""; + } + + /// If `sc` corresponds to a "closure-like" function (as defined in + /// `GetParentNameIfClosure`), returns the parent Function* + /// where a variable named `variable_name` exists. + /// Returns nullptr if `sc` is not a closure, or if the query does + /// not make sense for this language. + Function *FindParentOfClosureWithVariable(llvm::StringRef variable_name, + const SymbolContext &sc) const; + // END SWIFT + protected: // Classes that inherit from Language can see and modify these diff --git a/lldb/source/Plugins/ExpressionParser/Swift/SwiftUserExpression.cpp b/lldb/source/Plugins/ExpressionParser/Swift/SwiftUserExpression.cpp index 18f94d832a56c..900978bf98f7d 100644 --- a/lldb/source/Plugins/ExpressionParser/Swift/SwiftUserExpression.cpp +++ b/lldb/source/Plugins/ExpressionParser/Swift/SwiftUserExpression.cpp @@ -35,6 +35,7 @@ #include "lldb/Symbol/Type.h" #include "lldb/Symbol/Variable.h" #include "lldb/Symbol/VariableList.h" +#include "lldb/Target/Language.h" #include "lldb/Utility/LLDBAssert.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" @@ -693,6 +694,68 @@ SwiftUserExpression::GetTextAndSetExpressionParser( return parse_result; } +/// If `sc` represents a "closure-like" function according to `lang`, and +/// `var_name` can be found in a parent context, create a diagnostic +/// explaining that this variable is available but not captured by the closure. +static std::string +CreateVarInParentScopeDiagnostic(StringRef var_name, + StringRef parent_func_name) { + return llvm::formatv("A variable named '{0}' existed in function '{1}', but " + "it was not captured in the closure.\nHint: the " + "variable may be available in a parent frame.", + var_name, parent_func_name); +} + +/// If `diagnostic_manager` contains a "cannot find in scope" +/// diagnostic, attempt to enhance it by showing if `var_name` is used inside a +/// closure, not captured, but defined in a parent scope. +static void EnhanceNotInScopeDiagnostics(DiagnosticManager &diagnostic_manager, + ExecutionContextScope *exe_scope) { + if (!exe_scope) + return; + lldb::StackFrameSP stack_frame = exe_scope->CalculateStackFrame(); + if (!stack_frame) + return; + SymbolContext sc = + stack_frame->GetSymbolContext(lldb::eSymbolContextEverything); + Language *swift_lang = + Language::FindPlugin(lldb::LanguageType::eLanguageTypeSwift); + if (!swift_lang) + return; + + static const RegularExpression not_in_scope_regex = + RegularExpression("(.*): cannot find '([^']+)' in scope\n(.*)"); + for (auto &diag : diagnostic_manager.Diagnostics()) { + if (!diag) + continue; + + llvm::SmallVector match_groups; + + if (StringRef old_rendered_msg = diag->GetDetail().rendered; + !not_in_scope_regex.Execute(old_rendered_msg, &match_groups)) + continue; + + StringRef prefix = match_groups[1]; + StringRef var_name = match_groups[2]; + StringRef suffix = match_groups[3]; + + Function *parent_func = + swift_lang->FindParentOfClosureWithVariable(var_name, sc); + if (!parent_func) + continue; + std::string new_message = CreateVarInParentScopeDiagnostic( + var_name, parent_func->GetDisplayName()); + + std::string new_rendered = + llvm::formatv("{0}: {1}\n{2}", prefix, new_message, suffix); + const DiagnosticDetail &old_detail = diag->GetDetail(); + diag = std::make_unique( + diag->getKind(), diag->GetCompilerID(), + DiagnosticDetail{old_detail.source_location, old_detail.severity, + std::move(new_message), std::move(new_rendered)}); + } +} + bool SwiftUserExpression::Parse(DiagnosticManager &diagnostic_manager, ExecutionContext &exe_ctx, lldb_private::ExecutionPolicy execution_policy, @@ -888,6 +951,7 @@ bool SwiftUserExpression::Parse(DiagnosticManager &diagnostic_manager, fixed_expression.substr(fixed_start, fixed_end - fixed_start); } } + EnhanceNotInScopeDiagnostics(diagnostic_manager, exe_scope); return false; case ParseResult::success: llvm_unreachable("Success case is checked separately before switch!"); diff --git a/lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp b/lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp index 938eb646d2574..18da6408a0afd 100644 --- a/lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp +++ b/lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp @@ -2001,6 +2001,11 @@ SwiftLanguage::AreEqualForFrameComparison(const SymbolContext &sc1, llvm_unreachable("unhandled enumeration in AreEquivalentFunctions"); } +std::string +SwiftLanguage::GetParentNameIfClosure(llvm::StringRef mangled_name) const { + return SwiftLanguageRuntime::GetParentNameIfClosure(mangled_name); +} + //------------------------------------------------------------------ // Static Functions //------------------------------------------------------------------ diff --git a/lldb/source/Plugins/Language/Swift/SwiftLanguage.h b/lldb/source/Plugins/Language/Swift/SwiftLanguage.h index c93743f595661..998c8d74284d6 100644 --- a/lldb/source/Plugins/Language/Swift/SwiftLanguage.h +++ b/lldb/source/Plugins/Language/Swift/SwiftLanguage.h @@ -81,6 +81,9 @@ class SwiftLanguage : public Language { std::optional AreEqualForFrameComparison(const SymbolContext &sc1, const SymbolContext &sc2) const override; + + std::string + GetParentNameIfClosure(llvm::StringRef mangled_name) const override; //------------------------------------------------------------------ // Static Functions //------------------------------------------------------------------ diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h index 6e6b63de755da..02e9d4d4871e8 100644 --- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h +++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h @@ -205,6 +205,8 @@ class SwiftLanguageRuntime : public LanguageRuntime { const SymbolContext *sc = nullptr, const ExecutionContext *exe_ctx = nullptr); + static std::string GetParentNameIfClosure(llvm::StringRef mangled_name); + /// Demangle a symbol to a swift::Demangle node tree. /// /// This is a central point of access, for purposes such as logging. diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp index 1433cc79021f0..4a64d550baacd 100644 --- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp +++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +#include "Plugins/TypeSystem/Swift/SwiftASTContext.h" +#include "Plugins/TypeSystem/Swift/SwiftDemangle.h" #include "SwiftLanguageRuntime.h" #include "lldb/Breakpoint/StoppointCallbackContext.h" @@ -1496,4 +1498,30 @@ SwiftLanguageRuntime::GetGenericSignature(llvm::StringRef function_name, return signature; } +std::string SwiftLanguageRuntime::GetParentNameIfClosure(StringRef name) { + using Kind = Node::Kind; + swift::Demangle::Context ctx; + auto *node = SwiftLanguageRuntime::DemangleSymbolAsNode(name, ctx); + if (!node || node->getKind() != Node::Kind::Global) + return ""; + + // Replace the top level closure node with the child function-like node, and + // attempt to remangle. If successful, this produces the parent function-like + // entity. + static const auto closure_kinds = {Kind::ImplicitClosure, + Kind::ExplicitClosure}; + static const auto function_kinds = {Kind::ImplicitClosure, + Kind::ExplicitClosure, Kind::Function}; + auto *closure_node = swift_demangle::GetFirstChildOfKind(node, closure_kinds); + auto *parent_func_node = + swift_demangle::GetFirstChildOfKind(closure_node, function_kinds); + if (!parent_func_node) + return ""; + swift_demangle::ReplaceChildWith(*node, *closure_node, *parent_func_node); + + if (ManglingErrorOr mangled = swift::Demangle::mangleNode(node); + mangled.isSuccess()) + return mangled.result(); + return ""; +} } // namespace lldb_private diff --git a/lldb/source/Plugins/TypeSystem/Swift/SwiftDemangle.h b/lldb/source/Plugins/TypeSystem/Swift/SwiftDemangle.h index 0887ad8e2d5e0..fa5ec5afc0095 100644 --- a/lldb/source/Plugins/TypeSystem/Swift/SwiftDemangle.h +++ b/lldb/source/Plugins/TypeSystem/Swift/SwiftDemangle.h @@ -22,6 +22,32 @@ namespace lldb_private { namespace swift_demangle { +using NodePointer = swift::Demangle::NodePointer; +using Node = swift::Demangle::Node; + +/// Returns the first child of `node` whose kind is in `kinds`. +inline NodePointer GetFirstChildOfKind(NodePointer node, + llvm::ArrayRef kinds) { + if (!node) + return nullptr; + for (auto *child : *node) + if (llvm::is_contained(kinds, child->getKind())) + return child; + return nullptr; +} + +/// Assumes that `to_replace` is a child of `parent`, and replaces it with +/// `new_child`. The Nodes must all be owned by the same context. +inline void ReplaceChildWith(Node &parent, Node &to_replace, Node &new_child) { + for (unsigned idx = 0; idx < parent.getNumChildren(); idx++) { + auto *child = parent.getChild(idx); + if (child == &to_replace) + parent.replaceChild(idx, &new_child); + return; + } + llvm_unreachable("invalid child passed to replaceChildWith"); +} + /// Access an inner node by following the given Node::Kind path. /// /// Note: The Node::Kind path is relative to the given root node. The root diff --git a/lldb/source/Symbol/Variable.cpp b/lldb/source/Symbol/Variable.cpp index 6b206d3b3620d..35d0842ad0a9f 100644 --- a/lldb/source/Symbol/Variable.cpp +++ b/lldb/source/Symbol/Variable.cpp @@ -291,28 +291,9 @@ bool Variable::IsInScope(StackFrame *frame) { // this variable was defined in is currently Block *deepest_frame_block = frame->GetSymbolContext(eSymbolContextBlock).block; - if (deepest_frame_block) { - SymbolContext variable_sc; - CalculateSymbolContext(&variable_sc); - - // Check for static or global variable defined at the compile unit - // level that wasn't defined in a block - if (variable_sc.block == nullptr) - return true; - - // Check if the variable is valid in the current block - if (variable_sc.block != deepest_frame_block && - !variable_sc.block->Contains(deepest_frame_block)) - return false; - - // If no scope range is specified then it means that the scope is the - // same as the scope of the enclosing lexical block. - if (m_scope_range.IsEmpty()) - return true; - - addr_t file_address = frame->GetFrameCodeAddress().GetFileAddress(); - return m_scope_range.FindEntryThatContains(file_address) != nullptr; - } + Address frame_addr = frame->GetFrameCodeAddress(); + if (deepest_frame_block) + return IsInScope(*deepest_frame_block, frame_addr); } break; @@ -322,6 +303,27 @@ bool Variable::IsInScope(StackFrame *frame) { return false; } +bool Variable::IsInScope(const Block &block, const Address &addr) { + SymbolContext variable_sc; + CalculateSymbolContext(&variable_sc); + + // Check for static or global variable defined at the compile unit + // level that wasn't defined in a block + if (variable_sc.block == nullptr) + return true; + + // Check if the variable is valid in the current block + if (variable_sc.block != &block && !variable_sc.block->Contains(&block)) + return false; + + // If no scope range is specified then it means that the scope is the + // same as the scope of the enclosing lexical block. + if (m_scope_range.IsEmpty()) + return true; + + return m_scope_range.FindEntryThatContains(addr.GetFileAddress()) != nullptr; +} + Status Variable::GetValuesForVariableExpressionPath( llvm::StringRef variable_expr_path, ExecutionContextScope *scope, GetVariableCallback callback, void *baton, VariableList &variable_list, diff --git a/lldb/source/Target/Language.cpp b/lldb/source/Target/Language.cpp index 86754c251cd93..942b4140c4fc3 100644 --- a/lldb/source/Target/Language.cpp +++ b/lldb/source/Target/Language.cpp @@ -14,8 +14,10 @@ #include "lldb/Core/PluginManager.h" #include "lldb/Interpreter/OptionValueProperties.h" +#include "lldb/Symbol/CompileUnit.h" #include "lldb/Symbol/SymbolFile.h" #include "lldb/Symbol/TypeList.h" +#include "lldb/Symbol/VariableList.h" #include "lldb/Target/Target.h" #include "lldb/Utility/Stream.h" @@ -574,3 +576,165 @@ bool SourceLanguage::IsObjC() const { bool SourceLanguage::IsCPlusPlus() const { return name == llvm::dwarf::DW_LNAME_C_plus_plus; } + +// BEGIN SWIFT +// Implement LanguageCPlusPlus::GetParentNameIfClosure and upstream this. +// rdar://152321823 + +/// If `sc` represents a "closure"-like function (according to +/// Language::GetParentNameIfClosure), returns sc.function and all parent +/// functions up to and including the first non-closure-like function. If `sc` +/// is not a closure, or if the query does not make sense for `language`, +/// returns a list containing only sc.function. +static llvm::SmallVector +GetParentFunctionsWhileClosure(const SymbolContext &sc, + const Language &language) { + // The algorithm below terminates on the assumption that + // `GetParentNameIfClosure` produces an empty string when composing that + // function with itself enough times. For safety, define an upper limit. + constexpr auto upper_limit = 8; + + llvm::SmallVector parents; + Function *root = sc.function; + if (root == nullptr) + return parents; + + parents.push_back(root); + for (int idx = 0; idx < upper_limit; idx++) { + ConstString mangled = root->GetMangled().GetMangledName(); + std::string parent = language.GetParentNameIfClosure(mangled); + if (parent.empty()) + break; + + // Find the enclosing function, if it exists. + SymbolContextList sc_list; + Module::LookupInfo lookup_info( + ConstString(parent), lldb::FunctionNameType::eFunctionNameTypeFull, + lldb::eLanguageTypeSwift); + sc.module_sp->FindFunctions(lookup_info, CompilerDeclContext(), + ModuleFunctionSearchOptions(), sc_list); + if (sc_list.GetSize() != 1 || sc_list[0].function == nullptr) + break; + parents.push_back(sc_list[0].function); + root = sc_list[0].function; + } + return parents; +} + +/// Scans the line table of `function` looking for the first entry whose line +/// number is `line_number`. If no such entry is found, returns the entry +/// closest to but after `line_number`. +static std::optional
FindAddressForLineNumber(Function &function, + uint32_t line_number) { + CompileUnit *cu = function.GetCompileUnit(); + LineTable *line_table = cu ? cu->GetLineTable() : nullptr; + if (line_table == nullptr) + return std::nullopt; + + // Get the first line entry for this function. + AddressRange func_range = function.GetAddressRange(); + uint32_t first_entry_idx; + { + LineEntry first_line_entry; + line_table->FindLineEntryByAddress(func_range.GetBaseAddress(), + first_line_entry, &first_entry_idx); + } + + LineEntry best_match; + for (uint32_t entry_idx = first_entry_idx; entry_idx < line_table->GetSize(); + entry_idx++) { + LineEntry next_line; + line_table->GetLineEntryAtIndex(entry_idx, next_line); + + // Stop if this entry is outside the range of `function`. + Address base_addr = next_line.range.GetBaseAddress(); + if (!func_range.ContainsFileAddress(base_addr)) + break; + + // Stop on an exact match. + if (next_line.line == line_number) { + best_match = next_line; + break; + } + + // Otherwise, keep track of the best match so far. + if (next_line.line > line_number && next_line.line < best_match.line) + best_match = next_line; + } + + return best_match.range.GetBaseAddress(); +} + +/// Given a list of functions, returns a map: Function -> VariableList +/// containing local variables of each function. +static std::map +GetFuncToLocalVariablesMap(llvm::ArrayRef funcs) { + std::map map; + for (Function *function : funcs) { + VariableList &variable_list = map[function]; + Block &block = function->GetBlock(true /*can_create=*/); + block.AppendBlockVariables( + true /*can_create=*/, true /*get_child_block_variables=*/, + true /*stop_if_child_block_is_inlined_function=*/, + [](Variable *v) { return true; }, &variable_list); + } + return map; +} + +/// Returns the first line associated with `function`. +static uint32_t GetLineNumberForFunction(Function &function) { + FileSpec filespec; + uint32_t line_num = 0; + function.GetStartLineSourceInfo(filespec, line_num); + return line_num; +} + +/// Checks if `var` is in scope inside `function` at line `line_number`. +/// If this check can't be done, a best-effort comparison of: +/// line_number >= var.line_number +/// is performed. +static bool IsVariableInScopeAtLine(uint32_t line_number, + std::optional
line_addr, + Variable &var) { + auto fallback_line_comp = [&] { + return line_number >= var.GetDeclaration().GetLine(); + }; + + if (!line_addr) + return fallback_line_comp(); + + Block *defining_block = line_addr->CalculateSymbolContextBlock(); + if (defining_block) + return var.IsInScope(*defining_block, *line_addr); + return fallback_line_comp(); +} + +Function *Language::FindParentOfClosureWithVariable( + llvm::StringRef variable_name, const SymbolContext &closure_sc) const { + llvm::SmallVector function_chain = + GetParentFunctionsWhileClosure(closure_sc, *this); + if (function_chain.empty()) + return nullptr; + + std::map func_to_locals = + GetFuncToLocalVariablesMap(function_chain); + + llvm::ArrayRef children = + llvm::ArrayRef(function_chain).drop_back(); + llvm::ArrayRef parents = + llvm::ArrayRef(function_chain).drop_front(); + + for (auto [parent, child] : llvm::zip_equal(parents, children)) { + VariableList &parent_variables = func_to_locals[parent]; + uint32_t child_line_number = GetLineNumberForFunction(*child); + std::optional
parent_line_addr = + FindAddressForLineNumber(*parent, child_line_number); + + for (const VariableSP &var : parent_variables) + if (var->GetName() == variable_name && + IsVariableInScopeAtLine(child_line_number, parent_line_addr, *var)) + return parent; + } + return nullptr; +} +// END SWIFT diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp index 98be6ed4499ed..c7b0abfe7a262 100644 --- a/lldb/source/Target/StackFrame.cpp +++ b/lldb/source/Target/StackFrame.cpp @@ -22,6 +22,7 @@ #include "lldb/Symbol/VariableList.h" #include "lldb/Target/ABI.h" #include "lldb/Target/ExecutionContext.h" +#include "lldb/Target/Language.h" #include "lldb/Target/LanguageRuntime.h" #include "lldb/Target/Process.h" #include "lldb/Target/RegisterContext.h" @@ -475,6 +476,31 @@ VariableList *StackFrame::GetVariableList(bool get_file_globals, return m_variable_list_sp.get(); } +// BEGIN SWIFT +// Implement LanguageCPlusPlus::GetParentNameIfClosure and upstream this. +// rdar://152321823 + +/// If `sc` represents a "closure-like" function according to `lang`, and +/// `missing_var_name` can be found in a parent context, create a diagnostic +/// explaining that this variable is available but not captured by the closure. +static std::string +GetVariableNotCapturedDiagnostic(SymbolContext &sc, SourceLanguage lang, + ConstString missing_var_name) { + Language *lang_plugin = Language::FindPlugin(lang.AsLanguageType()); + if (lang_plugin == nullptr) + return ""; + Function *parent_func = + lang_plugin->FindParentOfClosureWithVariable(missing_var_name, sc); + if (!parent_func) + return ""; + return llvm::formatv("A variable named '{0}' existed in function '{1}', but " + "it was not captured in the closure.\nHint: the " + "variable may be available in a parent frame.", + missing_var_name, + parent_func->GetDisplayName().GetStringRef()); +} +// END SWIFT + VariableListSP StackFrame::GetInScopeVariableList(bool get_file_globals, bool must_have_valid_location) { @@ -620,6 +646,15 @@ ValueObjectSP StackFrame::GetValueForVariableExpressionPath( return valobj_sp; } if (!valobj_sp) { + // BEGIN SWIFT + // Implement LanguageCPlusPlus::GetParentNameIfClosure and upstream this. + // rdar://152321823 + if (std::string message = GetVariableNotCapturedDiagnostic( + m_sc, GetLanguage(), name_const_string); + !message.empty()) + error = Status::FromErrorString(message.c_str()); + else + // END SWIFT error = Status::FromErrorStringWithFormatv( "no variable named '{0}' found in this frame", name_const_string); return ValueObjectSP(); diff --git a/lldb/test/API/lang/swift/closures_var_not_captured/Makefile b/lldb/test/API/lang/swift/closures_var_not_captured/Makefile new file mode 100644 index 0000000000000..2a69023633b34 --- /dev/null +++ b/lldb/test/API/lang/swift/closures_var_not_captured/Makefile @@ -0,0 +1,3 @@ +SWIFT_SOURCES := main.swift + +include Makefile.rules diff --git a/lldb/test/API/lang/swift/closures_var_not_captured/TestSwiftClosureVarNotCaptured.py b/lldb/test/API/lang/swift/closures_var_not_captured/TestSwiftClosureVarNotCaptured.py new file mode 100644 index 0000000000000..5f726f6bffc8d --- /dev/null +++ b/lldb/test/API/lang/swift/closures_var_not_captured/TestSwiftClosureVarNotCaptured.py @@ -0,0 +1,105 @@ +""" +Test that we can print and call closures passed in various contexts +""" + +import os +import re +import lldb +import lldbsuite.test.lldbutil as lldbutil +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +def check_not_captured_error(test, frame, var_name, parent_function): + expected_error = ( + f"A variable named '{var_name}' existed in function '{parent_function}'" + ) + value = frame.EvaluateExpression(var_name) + error = value.GetError().GetCString() + test.assertIn(expected_error, error) + + value = frame.EvaluateExpression(f"1 + {var_name} + 1") + error = value.GetError().GetCString() + test.assertIn(expected_error, error) + + test.expect(f"frame variable {var_name}", substrs=[expected_error], error=True) + + +def check_no_enhanced_diagnostic(test, frame, var_name): + forbidden_str = "A variable named" + value = frame.EvaluateExpression(var_name) + error = value.GetError().GetCString() + test.assertNotIn(forbidden_str, error) + + value = frame.EvaluateExpression(f"1 + {var_name} + 1") + error = value.GetError().GetCString() + test.assertNotIn(forbidden_str, error) + + test.expect( + f"frame variable {var_name}", + substrs=[forbidden_str], + matching=False, + error=True, + ) + + +class TestSwiftClosureVarNotCaptured(TestBase): + def get_to_bkpt(self, bkpt_name): + return lldbutil.run_to_source_breakpoint( + self, bkpt_name, lldb.SBFileSpec("main.swift") + ) + + @swiftTest + def test_simple_closure(self): + self.build() + (target, process, thread, bkpt) = self.get_to_bkpt("break_simple_closure") + check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_1(arg:)") + check_not_captured_error(self, thread.frames[0], "arg", "func_1(arg:)") + check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me") + + @swiftTest + def test_nested_closure(self): + self.build() + (target, process, thread, bkpt) = self.get_to_bkpt("break_double_closure_1") + check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_2(arg:)") + check_not_captured_error(self, thread.frames[0], "arg", "func_2(arg:)") + check_not_captured_error( + self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_2(arg:)" + ) + check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me") + + lldbutil.continue_to_source_breakpoint( + self, process, "break_double_closure_2", lldb.SBFileSpec("main.swift") + ) + check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_2(arg:)") + check_not_captured_error(self, thread.frames[0], "arg", "func_2(arg:)") + check_not_captured_error( + self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_2(arg:)" + ) + check_not_captured_error( + self, thread.frames[0], "shadowed_var", "closure #1 in func_2(arg:)" + ) + check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me") + + @swiftTest + # Async variable inspection on Linux/Windows are still problematic. + @skipIf(oslist=["windows", "linux"]) + def test_async_closure(self): + self.build() + (target, process, thread, bkpt) = self.get_to_bkpt("break_async_closure_1") + check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_3(arg:)") + check_not_captured_error(self, thread.frames[0], "arg", "func_3(arg:)") + check_not_captured_error( + self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_3(arg:)" + ) + check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me") + + lldbutil.continue_to_source_breakpoint( + self, process, "break_async_closure_2", lldb.SBFileSpec("main.swift") + ) + check_not_captured_error(self, thread.frames[0], "var_in_foo", "func_3(arg:)") + check_not_captured_error(self, thread.frames[0], "arg", "func_3(arg:)") + check_not_captured_error( + self, thread.frames[0], "var_in_outer_closure", "closure #1 in func_3(arg:)" + ) + check_no_enhanced_diagnostic(self, thread.frames[0], "dont_find_me") diff --git a/lldb/test/API/lang/swift/closures_var_not_captured/main.swift b/lldb/test/API/lang/swift/closures_var_not_captured/main.swift new file mode 100644 index 0000000000000..21b8ede45de68 --- /dev/null +++ b/lldb/test/API/lang/swift/closures_var_not_captured/main.swift @@ -0,0 +1,92 @@ +func func_1(arg: Int) { + let var_in_foo = "Alice" + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let simple_closure = { + print("Hi there!") // break_simple_closure + } + simple_closure() + let dont_find_me = "haha" + print(dont_find_me) +} + +func func_2(arg: Int) { + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let var_in_foo = "Alice" + let shadowed_var = "shadow" + let outer_closure = { + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let var_in_outer_closure = "Alice" + let shadowed_var = "shadow2" + let inner_closure_1 = { + print("Hi inside!") // break_double_closure_1 + } + inner_closure_1() + + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let inner_closure_2 = { + print("Hi inside!") // break_double_closure_2 + } + inner_closure_2() + let dont_find_me = "haha" + print(dont_find_me) + } + let dont_find_me = "haha" + print(dont_find_me) + outer_closure() +} + +func func_3(arg: Int) async { + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let var_in_foo = "Alice" + + // FIXME: if we comment the line below, the test fails. For some reason, + // without this line, most variables don't have debug info in the entry + // funclet, which is the "parent name" derived from the closure name. + // rdar://152271048 + try! await Task.sleep(for: .seconds(0)) + + let outer_closure = { + do { + let dont_find_me = "haha" + print(dont_find_me) + } + let var_in_outer_closure = "Alice" + + let inner_closure_1 = { + print("Hi inside!") // break_async_closure_1 + } + inner_closure_1() + + try await Task.sleep(for: .seconds(0)) + + let inner_closure_2 = { + print("Hi inside!") // break_async_closure_2 + } + inner_closure_2() + let dont_find_me = "haha" + print(dont_find_me) + } + + try! await outer_closure() + let dont_find_me = "haha" + print(dont_find_me) +} + +func_1(arg: 42) +func_2(arg: 42) +await func_3(arg: 42)