|
| 1 | +# DebugDescription Macro |
| 2 | + |
| 3 | +* Proposal: [SE-0440](0440-debug-description-macro.md) |
| 4 | +* Authors: [Dave Lee](https://github.com/kastiglione) |
| 5 | +* Review Manager: [Steve Canon](https://github.com/stephentyrone) |
| 6 | +* Status: **Active Review (July 5-July 16, 2024)** |
| 7 | +* Implementation: Present in `main` under experimental feature `DebugDescriptionMacro` [apple/swift#69626](https://github.com/apple/swift/pull/69626) |
| 8 | +* Review: ([pitch](https://forums.swift.org/t/pitch-debug-description-macro/67711))([review](https://forums.swift.org/t/se-0440-debugdescription-macro/72958)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +This proposal introduces `@DebugDescription`, a new debugging macro to the standard library, which lets data types specify a custom summary to be presented by the debugger. This macro brings improvements to the debugging experience, and simplifies the maintenance and delivery of debugger type summaries. It can be used in place of `CustomDebugStringConvertible` conformance, or in addition to, for custom use cases. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +Displaying data is a fundamental part of software development. Both the standard library and the debugger offer multiple ways of printing values - Swift's print and dump, and LLDB's p and po commands. These all share the ability to render an arbitrary value into human readable text. Out of the box, both the standard library and the debugger present data as a nested tree of property-value pairs. The similarities run deep, for example the standard library and the debugger provide control over how much of the tree is shown. This functionality requires no action from the developer. |
| 17 | + |
| 18 | +The utility of displaying a complete value depends on the size and complexity of the data type(s), or depends on the context the data is being presented. Displaying the entirety of a small/shallow structure is sufficient, but some data types reach sizes/complexities where the complete tree of data is too large to be useful. |
| 19 | + |
| 20 | +For types that are too large or complex, the standard library and debugger again both provide tools giving us control over how our data is displayed. In the standard library, Swift has the `CustomDebugStringConvertible` protocol, which allows types to represented not as the aforementioned property tree, but as an arbitrary string. Relatedly, Swift has `CustomReflectable`, which lets developers control the contents and structure of the rendered property tree. For brevity and convention, from this point on this document will refer to the `CustomDebugStringConvertible` and `CustomReflectable` protocols via their single properties: `debugDescription` and `customMirror` respectively. |
| 21 | + |
| 22 | +LLDB has analogous features, which are called Type Summaries (\~`debugDescription`) and Synthetic Children (\\~`customMirror`) respectively. However, Swift and the debugger don't share or interoperate these definitions. Implementing these customizing protocols provides limited benefit inside the debugger. Likewise, defining Type Summaries or Synthetic Children in LLDB will have no benefit to Swift. |
| 23 | + |
| 24 | +While LLDB’s po command provides a convenient way to evaluate a `debugDescription` property defined in Swift, there are downsides to expression evaluation: Running arbitrary code can have side effects, be unstable to the application, and is slower. Expression evaluation happens by JIT compiling code, pushing it to the device the application is running on, and executing it in the context of the application, which involves a lot of work. As such, LLDB only does expression evaluation when explicitly requested by a user, most commonly with the po command in the console. Debugger UIs (IDEs) often provide a variable view which is populated using LLDB’s variable inspection which does not perform expression evaluation and is built on top of reflection. In some cases, such as when viewing crashlogs, or working with a core file, expression evaluation is not even possible. For these reasons, rendering values is ideally done without expression evaluation. |
| 25 | + |
| 26 | +This proposal introduces the ability to share a `debugDescription` definition between Swift and the debugger. This has benefits for developers, and for the debugger. |
| 27 | + |
| 28 | +LLDB Type Summaries can be defined using LLDB’s own (non Turing-complete) string interpolation syntax, called [Summary Strings](https://lldb.llvm.org/use/variable.html#summary-strings). While similar to Swift string interpolation, LLDB Summary Strings have restrictions that Swift string interpolation does not have. The primary restriction is that it allows data/property access, but not computation. LLDB Summary Strings cannot evaluate function calls, which includes computed properties. For the purpose of definition sharing, LLDB Type Summaries can be viewed as a lower common denominator of the two. As a result, definition sharing can be achieved only when a `debugDescription` definition meets the criteria imposed by LLDB Summary Strings. The criteria is not overly limiting, LLDB Summary Strings have been in for some time. |
| 29 | + |
| 30 | +Swift macros provide a convenient means to implement automatic translation of compatible `debugDescription` definitions into LLDB Summary Strings. A macro provides benefits that LLDB Summary Strings do not currently offer, including the ability to do compile time static validation to produce typo-free LLDB Summary Strings. The previously mentioned criteria that `debugDescription` must meet in order to be converted to an LLDB Summary String will loosen over time. This will be achieved first through the macro implementation becoming more sophisticated, and second as LLDB’s Summary Strings gain advancements. |
| 31 | + |
| 32 | +## Proposed solution |
| 33 | + |
| 34 | +Consider this simple example data type: |
| 35 | + |
| 36 | +```swift |
| 37 | +struct Organization: CustomDebugStringConvertible { |
| 38 | + var id: String |
| 39 | + var name: String |
| 40 | + var manager: Person |
| 41 | + var members: [Person] |
| 42 | + // ... and more |
| 43 | + |
| 44 | + var debugDescription: String { |
| 45 | + "#\(id) \(name) (\(manager.name))" |
| 46 | + } |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +To see the results of `debugDescription` in the debugger, the user has to run po team in the console. |
| 51 | + |
| 52 | +``` |
| 53 | +(lldb) po team |
| 54 | +"#15 Shipping (Francis Carlson) |
| 55 | +``` |
| 56 | + |
| 57 | +Running the p command, or viewing the value in the Debugger UI (IDE), will show the value’s property tree, which may have arbitrary size/nesting: |
| 58 | + |
| 59 | +``` |
| 60 | +(lldb) p team |
| 61 | +(Organization) { |
| 62 | + id = "..." |
| 63 | + name = "Shipping" |
| 64 | + manager = { |
| 65 | + name = "Francis Carlson" |
| 66 | + ... |
| 67 | + } |
| 68 | + members = { |
| 69 | + [0] = ... |
| 70 | + } |
| 71 | + ... |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +However, by introducing the `@DebugDescription` macro, we can teach the debugger how to generate a summary without expression evaluation. |
| 76 | + |
| 77 | +```swift |
| 78 | +@DebugDescription |
| 79 | +struct Organization: CustomDebugStringConvertible { |
| 80 | + var id: String |
| 81 | + var name: String |
| 82 | + var manager: Person |
| 83 | + var members: [Person] |
| 84 | + var officeAddress: [Address] |
| 85 | + // ... and more |
| 86 | + |
| 87 | + var debugDescription: String { |
| 88 | + "#\(id) \(name) (\(manager.name))" |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +The macro expands the body of `debugDescription` into the following LLDB Summary String: |
| 94 | + |
| 95 | +``` |
| 96 | +#${var.id} ${var.name} (${var.manage.name}) |
| 97 | +``` |
| 98 | + |
| 99 | +This summary string is emitted into the binary, where LLDB will load it automatically. Using this definition, LLDB can now present this description in contexts it previously could not, including the variable view and other parts of the debugger UI. |
| 100 | + |
| 101 | +A notable difference between the debugger console and debugger UI is that that UI displays one level at a time. When viewing an Array for example, its children are not expanded. To distinguish between elements of an Array (or any other collection), a user must expand each child. By employing `@DebugDescription`, LLDB will show a summary for each element of a collection, so that users may know – at a glance – exactly which element(s) to expand. |
| 102 | + |
| 103 | +## Detailed design |
| 104 | + |
| 105 | +```swift |
| 106 | +/// Converts description definitions to a debugger Type Summary. |
| 107 | +/// |
| 108 | +/// This macro converts compatible description implementations written in Swift |
| 109 | +/// to an LLDB format known as a Type Summary. A Type Summary is LLDB's |
| 110 | +/// equivalent to debugDescription, with the distinction that it does not |
| 111 | +/// execute code inside the debugged process. By avoiding code execution, |
| 112 | +/// descriptions can be produced faster, without potential side effects, and |
| 113 | +/// shown in situations where code execution is not performed, such as the |
| 114 | +/// variable list of an IDE. |
| 115 | +/// |
| 116 | +/// Consider this an example. This Team struct has a debugDescription which |
| 117 | +/// summarizes some key details, such as the team's name. The debugger only |
| 118 | +/// computes this string on demand - typically via the po command. By applying |
| 119 | +/// the DebugDescription macro, a matching Type Summary is constructed. This |
| 120 | +/// allows the user to show a string like "Rams [11-2]", without executing |
| 121 | +/// debugDescription. This improves the usability, performance, and |
| 122 | +/// reliability of the debugging experience. |
| 123 | +/// |
| 124 | +/// @DebugDescription |
| 125 | +/// struct Team: CustomDebugStringConvertible { |
| 126 | +/// var name: String |
| 127 | +/// var wins, losses: Int |
| 128 | +/// |
| 129 | +/// var debugDescription: String { |
| 130 | +/// "\(name) [\(wins)-\(losses)]" |
| 131 | +/// } |
| 132 | +/// } |
| 133 | +/// |
| 134 | +/// The DebugDescription macro supports both debugDescription, description, |
| 135 | +/// as well as a third option: a property named _debugDescription. The first |
| 136 | +/// two are implemented when conforming to the CustomDebugStringConvertible |
| 137 | +/// and CustomStringConvertible protocols. The additional _debugDescription |
| 138 | +/// property is useful when both debugDescription and description are |
| 139 | +/// implemented, but don't meet the requirements of the DebugDescription |
| 140 | +/// macro. If _debugDescription is implemented, DebugDescription choose it |
| 141 | +/// over debugDescription and description. Likewise, debugDescription is |
| 142 | +/// preferred over description. |
| 143 | +/// |
| 144 | +/// ### Description Requirements |
| 145 | +/// |
| 146 | +/// The description implementation has the following requirements: |
| 147 | +/// |
| 148 | +/// * The body of the description implementation must a single string |
| 149 | +/// expression. String concatenation is not supported, use string interpolation |
| 150 | +/// instead. |
| 151 | +/// * String interpolation can reference stored properties only, functions calls |
| 152 | +/// and other arbitrary computation are not supported. Of note, conditional |
| 153 | +/// logic and computed properties are not supported. |
| 154 | +/// * Overloaded string interpolation cannot be used. |
| 155 | +@attached(memberAttribute) |
| 156 | +public macro DebugDescription() = |
| 157 | + #externalMacro(module: "SwiftMacros", type: "DebugDescriptionMacro") |
| 158 | + |
| 159 | +/// Internal-only macro. See @DebugDescription. |
| 160 | +@attached(peer, names: named(lldb_summary)) |
| 161 | +public macro _DebugDescriptionProperty( debugIdentifier: String, _ computedProperties: [String]) = |
| 162 | + #externalMacro(module: "SwiftMacros", type: "_DebugDescriptionPropertyMacro") |
| 163 | +``` |
| 164 | + |
| 165 | +Of note, the work is split between two macros `@DebugDescription` and @_DebugDescriptionProperty. By design, `@DebugDescription` is attached to the type, where it gathers type-level information, including gather a list of stored properties. This macro also determines which description property to attach @_DebugDescriptionProperty to. |
| 166 | + |
| 167 | +@_DebugDescriptionProperty is not intended for direct use by users. This macro is scoped to the inspect a single description property, not the entire type. This approach of splitting the work allows the compiler to avoid unnecessary work. |
| 168 | + |
| 169 | +The support for `_debugDescription` in addition to `debugDescription` and `description` is to support two different use cases. |
| 170 | + |
| 171 | +First, in some cases, the existing `debugDescription`/`description` cannot be changed (where doing so would be a breaking change to either `String(reflecting:)` or `String(describing:)`). In these circumstances, developers can use `_debugDescription` instead. |
| 172 | + |
| 173 | +Second, there may be cases where a developer wishes to define an LLDB Summary String directly. Since `_debugDescription` is not coupled to existing API, developers can choose to include LLDB Summary String syntax directly in their implementation of `_debugDescription`. Note that the macro does not process LLDB Summary String syntax. Any explicit use of LLDB Summary String syntax is opaque to the macro. Just like any other string literal contents, it's passed through to LLDB. |
| 174 | + |
| 175 | +Using both `debugDescription` and `_debugDescription` is an intended use case. The design of this macro allows developers to have both an LLDB compatible `_debugDescription`, and a more complex `debugDescription`. This allows the debugger to show summary, while providing enabling a more detailed or dynamic `debugDescription`. |
| 176 | + |
| 177 | +## Source compatibility |
| 178 | + |
| 179 | +This proposal adds a new macro to the standard library. There are no source compatibility concerns. |
| 180 | + |
| 181 | +## ABI compatibility |
| 182 | + |
| 183 | +The macro implementation emits metadata for the debugger, and does not affect ABI. |
| 184 | + |
| 185 | +## Implications on adoption |
| 186 | + |
| 187 | +The macro can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility. |
| 188 | + |
| 189 | +## Future directions |
| 190 | + |
| 191 | +Future directions include generating Python instead of LLDB Summary Strings. This has the benefit of having fewer restrictions on the `debugDescription` definition. It has the downside of needing security scrutiny not required by LLDB Summary Strings. |
| 192 | + |
| 193 | +A similar future direction is to support sharing Swift `customMirror` definitions into LLDB Synthetic Children definitions. Unlike LLDB Type Summaries, LLDB has no "DSL" to expression LLDB Synthetic Children, currently the main option is Python. Given that there are two uses solved by generating Python, it's an approach worth considering in the future. While `customMirror` implementations are less common in Swift than their `debugDescription` counterpart, in LLDB, Synthetic Children are as important, or even more important than Summary Strings. The reason is that Synthetic Children allow data types to express their data "interface" rather than their implementation. Consider types like Array and Dictionary, which often have implementation complexity that provides optimal performance, not for data simplicity. |
| 194 | + |
| 195 | +## Alternatives considered |
| 196 | + |
| 197 | +### Explicit LLDB Summary Strings |
| 198 | + |
| 199 | +The simplest macro implementation is one that performs no Swift-to-LLDB translation and directly accepts an LLDB Summary String. This approach requires users to know LLDB Summary String syntax, which while not complex, still presents a hinderance to adoption. Such a macro would could create redundancy: `debugDescription` and the separate LLDB Summary String. These would need to be manually kept in sync. |
| 200 | + |
| 201 | +### Independent Property (No `debugDescription` Reuse) |
| 202 | + |
| 203 | +Instead of leveraging existing `debugDescription`/`description` implementations, the `@DebugDescription` macro could use a completely separate property. |
| 204 | + |
| 205 | +Reusing existing `debugDescription` implementations makes a tradeoff that may not be obvious to developers. The benefit is a single definition, and getting more out of a well known idiom. The risk is comes from the requirements imposed by `@DebugDescription`. The requirements could lead to developers changing their existing implementation. Any changes to `debugDescription` will impact `String(reflecting:)`, and similarly changes to `description` will impact `String(describing:)`. |
| 206 | + |
| 207 | +The risk involving String conversion would be avoided by having the macro use an independent property. The macro would not support `debugDescription`/`description`. In this scenario, developers would be required to implement `_debugDescription`, even if the implementation is identical to the existing `debugDescription`. |
| 208 | + |
| 209 | +Our expectation is that mosst code, particularly application code, will not depend on String conversion (especially `String(reflecting:)`). For code that does depend on String conversion, it should have testing in place to catch breaking changes. Inside of an application, authors of code which has behavior that depends on String conversion initializers should already be aware of the consequences of changing `debugDescription`/`description`. Frameworks are a more challenging situation, where its authors are not always aware of if/how its clients depend on String conversion. |
| 210 | + |
| 211 | +The belief is that the benefits of reusing `debugDescription` will outweigh the downsides. Framework authors can make it a policy of their own to not reuse `debugDescription`, if they believe that presents a risk to clients of their framework. |
| 212 | + |
| 213 | +### Contextual Diagnostics |
| 214 | + |
| 215 | +To help address the potential risk around reuse of `debugDescription`, the macro could emit diagnostics that vary by the property being used. Specifically, if the developer implements `_debugDescription`, they will get the full diagnostics available, indicating how to fix its implementation. Conversely, when `debugDescription` is being reused, the diagnostics will not contain details of which requirements were not met, instead the diagnostics would tell the user that `debugDescription` is not compatible, and to define `_debugDescription` instead. This should make it less likely that the macro leads to changes affecting String conversion. |
| 216 | + |
| 217 | +## Acknowledgments |
| 218 | + |
| 219 | +Thank you to Doug Gregor and Alex Hoppen for their generous and helpful macro and swift-syntax guidance and PR reviews. Thank you to Adrian Prantl for many productive discussions and implementation ideas. Thank you to Kuba Mracek for implementing linkage macros which support this work. Thank you to Tony Parker and Steven Canon for their adoption feedback. |
0 commit comments