Skip to content

[LLDB] List structured bindings as frame variables #122028

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Skynet0 opened this issue Jan 8, 2025 · 4 comments · May be fixed by #122265
Open

[LLDB] List structured bindings as frame variables #122028

Skynet0 opened this issue Jan 8, 2025 · 4 comments · May be fixed by #122265
Labels

Comments

@Skynet0
Copy link

Skynet0 commented Jan 8, 2025

In reference to the C++17 feature https://en.cppreference.com/w/cpp/language/structured_binding.

I've searched quite a bit, but haven't found a thread discussing this on Discourse or GitHub.

I have the following program:

#include <map>

int main() {
  std::map<int, int> m = {{0, 1}, {2, 3}};
  int t = 0;
  for (const auto& [k, v] : m) {
    t += v;
  }
}

Running LLDB:

$ lldb ./a.out
(lldb) target create "./a.out"
Current executable set to '/Users/skynet/devel/tmp/a.out' (arm64).
(lldb) b 7
Breakpoint 1: where = a.out`main + 264 at sb2.cpp:7:10, address = 0x0000000100001340
(lldb) r
Process 88718 launched: '/Users/skynet/devel/tmp/a.out' (arm64)
Process 88718 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100001340 a.out`main at sb2.cpp:7:10
   4      std::map<int, int> m = {{0, 1}, {2, 3}};
   5      int t = 0;
   6      for (const auto& [k, v] : m) {
-> 7        t += v;
   8      }
   9    }
Target 0: (a.out) stopped.
(lldb) fr v
(std::map<int, int>) m = size=2 {
  [0] = (first = 0, second = 1)
  [1] = (first = 2, second = 3)
}
(int) t = 0
(lldb) fr v k
(const int &) 0x000000015370456c (&k = 0)
(lldb) fr v v
(const int &) 0x0000000153704570 (&v = 1)

Inside the loop, I'd expect to see k and v as int&. Instead, I need to manually evaluate them, also in the frame.

Developing on an M2 Macbook Pro.

Compiler:

Homebrew clang version 19.1.6
Target: arm64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /opt/homebrew/Cellar/llvm/19.1.6/bin
Configuration file: /opt/homebrew/etc/clang/arm64-apple-darwin23.cfg

LLDB: lldb version 19.1.6

@llvmbot
Copy link
Member

llvmbot commented Jan 8, 2025

@llvm/issue-subscribers-lldb

Author: Ethan Ordentlich (Skynet0)

In reference to the C++17 feature <https://en.cppreference.com/w/cpp/language/structured_binding>.

I've searched quite a bit, but haven't found a thread discussing this on Discourse or GitHub.

I have the following program:

#include &lt;map&gt;

int main() {
  std::map&lt;int, int&gt; m = {{0, 1}, {2, 3}};
  int t = 0;
  for (const auto&amp; [k, v] : m) {
    t += v;
  }
}

Running LLDB:

$ lldb ./a.out
(lldb) target create "./a.out"
Current executable set to '/Users/skynet/devel/tmp/a.out' (arm64).
(lldb) b 7
Breakpoint 1: where = a.out`main + 264 at sb2.cpp:7:10, address = 0x0000000100001340
(lldb) r
Process 88718 launched: '/Users/skynet/devel/tmp/a.out' (arm64)
Process 88718 stopped
* thread #<!-- -->1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #<!-- -->0: 0x0000000100001340 a.out`main at sb2.cpp:7:10
   4      std::map&lt;int, int&gt; m = {{0, 1}, {2, 3}};
   5      int t = 0;
   6      for (const auto&amp; [k, v] : m) {
-&gt; 7        t += v;
   8      }
   9    }
Target 0: (a.out) stopped.
(lldb) fr v
(std::map&lt;int, int&gt;) m = size=2 {
  [0] = (first = 0, second = 1)
  [1] = (first = 2, second = 3)
}
(int) t = 0
(lldb) fr v k
(const int &amp;) 0x000000015370456c (&amp;k = 0)
(lldb) fr v v
(const int &amp;) 0x0000000153704570 (&amp;v = 1)

Inside the loop, I'd expect to see k and v as int&amp;. Instead, I need to manually evaluate them, also in the frame.

Developing on an M2 Macbook Pro.

Compiler:

Homebrew clang version 19.1.6
Target: arm64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /opt/homebrew/Cellar/llvm/19.1.6/bin
Configuration file: /opt/homebrew/etc/clang/arm64-apple-darwin23.cfg

LLDB: lldb version 19.1.6

@Michael137
Copy link
Member

Looks like we're still marking k and v in your example with DW_AT_artificial in DWARF:

0x00003ec8:         DW_TAG_variable
                      DW_AT_location    (DW_OP_breg31 WSP+56)
                      DW_AT_name        ("k")
                      DW_AT_type        (0x0000000000006eeb "const int &")
                      DW_AT_artificial  (true)

0x00003ed2:         DW_TAG_variable
                      DW_AT_location    (DW_OP_breg31 WSP+48)
                      DW_AT_name        ("v")
                      DW_AT_type        (0x0000000000003a61 "const int &")
                      DW_AT_artificial  (true)

which LLDB will omit from frame var output.

This should've been addressed by #48909, but looks like it doesn't quite work for this case

@Michael137
Copy link
Member

Michael137 commented Jan 8, 2025

This happens because std::pair is a tuple-like decomposition but doesn't define a std::pair::get member function. Instead (at least in libc++) we have std::get specializations for std::pair. So in the AST we get following VarDecl nodes:

VarDecl 0x12102d888 </Users/michaelbuch/bindings.cpp:13:16> col:16 implicit used k 'std::tuple_element<0, const std::tuple<int, int>>::type &' cinit
`-CallExpr 0x12102ceb8 <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type':'const int' lvalue adl
  |-ImplicitCastExpr 0x12102cea0 <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(*)(const tuple<int, int> &) noexcept' <FunctionToPointerDecay>
  | `-DeclRefExpr 0x12102c328 <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' lvalue Function 0x1210262d8 'get' 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' (FunctionTemplate 0x11d068088 'get')
  `-DeclRefExpr 0x121022dc8 <col:16> 'const std::tuple<int, int>' lvalue Decomposition 0x121021518 '' 'const std::tuple<int, int> &'

As opposed to following VarDecl if the decomposed type had a get member-function:

VarDecl 0x11d110cf8 </Users/michaelbuch/bindings2.cpp:23:10> col:10 implicit used z1 'std::tuple_element<0, B>::type &&' cinit                                                                                        
`-ExprWithCleanups 0x11d110dc8 <col:10> 'int' xvalue                                                                                                                                                                  
  `-MaterializeTemporaryExpr 0x11d110d60 <col:10> 'int' xvalue extended by Var 0x11d110cf8 'z1' 'std::tuple_element<0, B>::type &&'                                                                                   
    `-CXXMemberCallExpr 0x11d110780 <col:10> 'int'                                                                                                                                                                    
      `-MemberExpr 0x11d110718 <col:10> '<bound member function type>' .get 0x11d104390                                                                                                                               
        `-ImplicitCastExpr 0x11d1105a0 <col:10> 'B' xvalue <NoOp>                                                                                                                                                     
          `-DeclRefExpr 0x11d110580 <col:10> 'B' lvalue Decomposition 0x11d1100a8 '' 'B'                                                                                                                              

It looks like IgnoreUnlessSpelledInSource that we added in #100355 doesn't skip over regular CallExpr (only member function calls). Not sure why that is. But that's the reason we don't recognize this as a decomposed VarDecl when generating the debug-info for it.

@Michael137
Copy link
Member

Michael137 commented Jan 8, 2025

Self-contained reproducer:

namespace std {
struct B {
    int w;
    int z;

   // UNCOMMENT below to generate correct debug-info for the decomposition.
   // template<int> int get();
   // template<> int get<0>() { return w; }
   // template<> int get<1>() { return z; }
};

// Helpers to enable "tuple-like decomposition" for B
template <typename T>
struct tuple_size {};
template <>
struct tuple_size<B> {
    static constexpr unsigned value = 2;
};

template <unsigned, typename T>
struct tuple_element {
    using type = int;
};

template <unsigned I>
constexpr std::tuple_element<I, B>::type &&
get(B &&p) noexcept {
  if (I == 0)
    return static_cast<std::tuple_element<I, B>::type &&>(p.w);
  else
    return static_cast<std::tuple_element<I, B>::type &&>(p.z);
}
}  // namespace std

int main() {
    auto [z1, z2] = std::B{1, 2};
    return z1 + z2;
}

Michael137 added a commit to Michael137/llvm-project that referenced this issue Jan 9, 2025
… for std::get free function

When we generate the debug-info for a `VarDecl` we try
to determine whether it was introduced as part of a structure
binding (aka a "holding var"). If it was,
we don't mark it as `artificial`.

The heuristic to determine a holding var uses
`IgnoreUnlessSpelledInSource` to unwrap the `VarDecl` initializer
until we hit a `DeclRefExpr` that refers to a `Decomposition`.
For "tuple-like decompositions", Clang will generate a call to
a `template<size_t I> Foo get(Bar)` function that retrieves the
`Ith` element from the tuple-like structure. If that function is a
member function, we get an AST that looks as follows:
```
VarDecl implicit used z1 'std::tuple_element<0, B>::type &&' cinit
`-ExprWithCleanups <col:10> 'int' xvalue
  `-MaterializeTemporaryExpr <col:10> 'int' xvalue extended by Var 0x11d110cf8 'z1' 'std::tuple_element<0, B>::type &&'
    `-CXXMemberCallExpr <col:10> 'int'
      `-MemberExpr <col:10> '<bound member function type>' .get 0x11d104390
        `-ImplicitCastExpr <col:10> 'B' xvalue <NoOp>
          `-DeclRefExpr <col:10> 'B' lvalue Decomposition 0x11d1100a8 '' 'B'
```
`IgnoreUnlessSpelledInSource` happily unwraps this down to the
`DeclRefExpr`. However, when the `get` helper is a free function
(which it is for `std::pair` in libc++ for example), then the AST
is:
```
VarDecl col:16 implicit used k 'std::tuple_element<0, const std::tuple<int, int>>::type &' cinit
`-CallExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type':'const int' lvalue adl
  |-ImplicitCastExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(*)(const tuple<int, int> &) noexcept' <FunctionToPointerDecay>
  | `-DeclRefExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' lvalue Function 0x1210262d8 'get' 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' (FunctionTemplate 0x11d068088 'get')
  `-DeclRefExpr <col:16> 'const std::tuple<int, int>' lvalue Decomposition 0x121021518 '' 'const std::tuple<int, int> &'
```
`IgnoreUnlessSpelledInSource` doesn't unwrap this `CallExpr`, so we
incorrectly mark the binding as `artificial` in debug-info.

This patch adjusts our heuristic to account for `CallExpr` nodes.

Fixes llvm#122028
Michael137 added a commit to Michael137/llvm-project that referenced this issue Jan 13, 2025
… for std::get free function

When we generate the debug-info for a `VarDecl` we try
to determine whether it was introduced as part of a structure
binding (aka a "holding var"). If it was,
we don't mark it as `artificial`.

The heuristic to determine a holding var uses
`IgnoreUnlessSpelledInSource` to unwrap the `VarDecl` initializer
until we hit a `DeclRefExpr` that refers to a `Decomposition`.
For "tuple-like decompositions", Clang will generate a call to
a `template<size_t I> Foo get(Bar)` function that retrieves the
`Ith` element from the tuple-like structure. If that function is a
member function, we get an AST that looks as follows:
```
VarDecl implicit used z1 'std::tuple_element<0, B>::type &&' cinit
`-ExprWithCleanups <col:10> 'int' xvalue
  `-MaterializeTemporaryExpr <col:10> 'int' xvalue extended by Var 0x11d110cf8 'z1' 'std::tuple_element<0, B>::type &&'
    `-CXXMemberCallExpr <col:10> 'int'
      `-MemberExpr <col:10> '<bound member function type>' .get 0x11d104390
        `-ImplicitCastExpr <col:10> 'B' xvalue <NoOp>
          `-DeclRefExpr <col:10> 'B' lvalue Decomposition 0x11d1100a8 '' 'B'
```
`IgnoreUnlessSpelledInSource` happily unwraps this down to the
`DeclRefExpr`. However, when the `get` helper is a free function
(which it is for `std::pair` in libc++ for example), then the AST
is:
```
VarDecl col:16 implicit used k 'std::tuple_element<0, const std::tuple<int, int>>::type &' cinit
`-CallExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type':'const int' lvalue adl
  |-ImplicitCastExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(*)(const tuple<int, int> &) noexcept' <FunctionToPointerDecay>
  | `-DeclRefExpr <col:16> 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' lvalue Function 0x1210262d8 'get' 'const typename tuple_element<0UL, tuple<int, int>>::type &(const tuple<int, int> &) noexcept' (FunctionTemplate 0x11d068088 'get')
  `-DeclRefExpr <col:16> 'const std::tuple<int, int>' lvalue Decomposition 0x121021518 '' 'const std::tuple<int, int> &'
```
`IgnoreUnlessSpelledInSource` doesn't unwrap this `CallExpr`, so we
incorrectly mark the binding as `artificial` in debug-info.

This patch adjusts our heuristic to account for `CallExpr` nodes.

Fixes llvm#122028
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
4 participants