Pitch: Debug Description macro

Hello all,

We would like to add a macro, to the Swift standard library, that improves debugging in the following ways:

  • Debugger users will be able to see debug descriptions in circumstances they previously could not, such as Xcode’s variable view and crashlogs
  • Code authors can implement, in Swift, how the debugger summarizes their data type – via compatible debugDescription or description implementations

To demonstrate, consider an example:

struct Student: CustomDebugStringConvertible {
  var name: String
  var id: Int
  // more properties
  
  var debugDescription: String {
    "\(id): \(name)"
  }
}

To display this debug description, LLDB needs to perform expression evaluation. LLDB does expression evaluation on demand, commonly using the po command. Outside of explicit commands, expression evaluation is generally not performed, and in some cases not even possible.

Expression evaluation can be avoided in this case, by defining an LLDB Type Summary. The following manually constructed command creates a type summary using LLDB’s summary string format:

(lldb) type summary add -s '${var.id}: ${var.name}' PupilKit.Student

In this case, and many others, the Swift source of debugDescription can be converted to an LLDB type summary. Any debugDescription implementation that references only stored properties, should in theory be convertible.

We propose a macro which converts compatible implementations of debugDescription or description into LLDB summary strings, and will embed that string into the binary. LLDB will load these records automatically. An additional benefit of this reuse is authors can write unit test for their debug descriptions, to catch regressions.

In the above example, the change is the minor addition of the macro:

@DebugDescription
struct Student: CustomDebugStringConvertible {
  // same as before
}

The @DebugDescription macro generates global constants (via the peer role) that contains the type name, and the converted summary string. For demonstration only, the expanded macro might look like:

@_section("__DATA_CONST,__lldbsummaries")
let Student_lldb_summary = ("PupilKit.Student", "${var.id}: ${var.name}")

The reason this is demonstration only is that @_section globals do not support String values. Instead, the values emitted will be tuples of UInt8 (in UTF-8 encoding). Additionally, the implementation would support other platform specific section names (the above is Darwin specific).

When attached to an incompatible implementation of debugDescription or description, the macro will emit a warning or error. An incompatible implementation is one that requires expression evaluation, this includes function calls, initializers, arithmetic and other operators, casting, etc. Anything outside of property reads can be assumed to require expression evaluation. The initial version will support string literals, as they map to directly to summary strings. To support a wider range of implementations, such as those that construct strings using conditionals or loops (but still not function calls) the macro would instead generate an LLDB script. This functionality would be a future improvement.

Unfortunately, there are cases where the macro would be not be able to identify an incompatible implementation. A computed property, unlike a stored property, requires expression evaluation. At the AST level, some references to computed properties can appear indistinguishable from a reference to a stored property. This is an inherent constraint of macros and the scope of the AST that’s made available to them. When the macro is unable to identify use of a computed property at compile time, LLDB will emit a warning at debug time.

Thank you for your feedback and ideas!

22 Likes

Having just spent a couple of hours fighting with LLDB's Python interface to generate a type summary with some conditional logic, I think this would be fantastic.

The two most vexing issues I've run into with LLDB type summaries and Swift are:

  1. Getting the type names correct so that the summaries actually get used, especially with generics, which IIRC can require regular expressions in type summary add.
  2. Any sort of conditional logic, which by definition requires using LLDB's Python API.

For the former, it would be a big win to just not have to figure out what the type names are. Even better, it would be great to be able to generate debug descriptions for a generic type with the ability to provide an override for a specific specialization, e.g.

@DebugDescription
struct MyContainer<T>: CustomStringConvertible {
    var description: String {
        "generic"
    }
}

extension MyContainer where T == Int {
    var description: String {
        "specialized with int"
    }
}

I realize you're explicitly flagging conditionals and loops as future work and not part of this proposal, and I totally understand why. That said, I think @DebugDescription will get way more useful once they're included.

As an aside, for anyone who is trying to integrate an LLDB Python script with Xcode's project specific LLDBInitFile using relative paths (so that anyone who clones your repo will automatically get the Python script loaded), the flag you're looking for is "-c". Specifically command script import -c myscript.py. The "-c" flag will make LLDB use paths relative to the current lldbinit file being sourced. The only place I found this documented was in this commit.

2 Likes

I wonder if this makes sense for the standard library, or whether it should perhaps go in an LLDB-specific support library.

Would other debuggers support these descriptions?

By explicitly scoping this feature to be LLDB-specific, could we offer better integration between LLDB and Swift code, without having to worry about other debuggers?

1 Like

I'm not qualified to speak on Karl's question about whether this belongs in the standard library, and don't have strong opinions either way. I'd be excited to see this shipped in whatever form works best for Swift.

Leaving that to the side, here are two other real-world issues I’ve run into with type summaries:

Custom type summaries with Ranges

Consider a situation where you have a custom collection with an associated index:

struct MyString {
    struct Index {}
}

You make a type summary for MyString.Index (let's say "i0", "i1", "i2"), and would like Ranges of indices to print using your type summary (e.g. "i3..<i11")

The regex for for the Range type summary ^Swift.Range<.+>$, which does match Range<MyModule.MyString.Index>, but the C++ implementation that generates the summary has logic for deciding whether or not to actually generate one. Without knowing the internals of LLDB, it’s not clear what you have to do to get a Range<MyModule.MyString.Index> to print using MyString.Index's type summary.

This even shows up in the standard library: Range<Int> has a summary but Range<String.Index> doesn't:

let s = "hello"
let a = 5..<10
let b = s.startIndex..<s.endIndex
(lldb) p a
(Range<Int>) 5..<10
(lldb) p b
(Range<String.Index>) {
  lowerBound = 0[any]
  upperBound = 5[utf8]
}

A workaround is to define your own type summary that matches instead, but it’s somewhat of a pain to get right, and IIRC putting the summary in a category other than "default," might affect whether yours gets picked.

type summary add --summary-string "${var.lowerBound}..<${var.upperBound}" "Range<MyModule.MyString.Index>"

Type aliases

The Range issue gets more complicated when you have type aliases. Consider this expanded example:

struct MyString {
    struct Index {}
    struct UTF8View {}
}

extension MyString.UTF8View {
    typealias Index = MyString.Index
}

struct MySubstring {
    typealias Index = MyString.Index
}

The good news is that a type summary for MyModule.MyString.Index matches even if you’re looking at a value with a static type of MyModule.MySubstring.Index or MyModule.MyString.UTF8View.Index, but the Range issue compounds. You need to have custom summaries for Range<MyModule.MyString.Index>, Range<MyModule.MyString.UTF8View.Index> and Range<MyModule.MySubstring.Index>.

You can use one summary for the first two by switching the type name to the regex ^Range<MyModule.MyString.+Index>$ (though given how dots are used in these examples, you might have to stare at it for a second to see why it matches both), but the third has to be matched separately – a simple non-regex matcher will do: "Range<MyModule.MySubstring.Index>"

The good news is that all of these are solvable problems, but they’re annoying, especially if you’re trying for the first time – even more pedestrian things like realizing that the Swift module is special and you don't need to qualify types with "Swift.", but you do need to qualify your own types with "MyModule." can slow things down, and the feedback loop isn't particularly fast. It would be great if everything just worked.

1 Like