[Pitch #2] DebugDescription macro

Hello Swift Community,

This is a follow up to a previous pitch. This update includes an evolution proposal document which can be read at swift-evolution/proposals/xxxx-debug-description-macro.md at Propose-DebugDescription-macro · kastiglione/swift-evolution · GitHub. The proposal describes in more detail the motivation and purpose of this macro.

See also the corresponding draft PR.

I look forward to the feedback discussion. Thank you!

Dave

6 Likes

I’ve been wanting something like this forever, so overall +1.

It seems weird that the ”last resort” property is called _debugDescription. Having both debugDescription and _debugDescription will be confusing, and the underscore implies that _debugDescription is something you shouldn’t use.

Could the ”last resort” property have a more descriptive name like debugSummmary or lldbDebugDescription?

4 Likes

Only the macro should use _debugDescription. In that sense, the _ prefix may be appropriate.

That said, I am open to renaming. I like debugSummary, but wonder the existence of both debugDescription and debugSummary could also be confusing.

1 Like

Is debuggerDescription still too similar?

Thinking out loud about the _...

I was thinking if perhaps $ would be better since we use it in a lot of macro/compiler synthesized fields etc. For example, the @TaskLocal and @Resolvable macros in stdlib both introduce new members and types prefixed with $. But then I realize that those both (e.g. $taskLocal, $MyActor) are intended to be reached to by users -- so that's not good for this _debugDescription then, and honestly the _ probably conveys the right meaning...

Might consider _debuggerDescription if it makes it clearer, but the leading _, I think, is actually a good call for this method.

Looking forward to this landing!

3 Likes

Will this macro always emit symbols and the linker sections unconditionally, regardless of build mode?

I don't think we have capabilities in the macro system to inspect the compilation context, and while the Swift compiler itself doesn't have a standard definition of "what is a debug build", -DDEBUG is a fairly well-adopted pattern. Can the macro-generated code be wrapped in #if DEBUG blocks so that it's only injected in debug builds?

The other options are less satisfying—I wouldn't want this metadata taking up space in my shipping release builds, and making users write the following also seems like a usability downgrade:

#if DEBUG
@DebugDescription
#endif
public struct X { ... }
3 Likes

@allevato to clarify, is the concern you have about binary size, information leakage, both, or something else?

Both binary size and information leakage, yes.

We have a lot of metadata in Swift binaries that is difficult or impossible to strip today when it's not actually used because the linker can't figure that out based on how it's structured/represented. In this case, the feature is designed specifically to be used by LLDB. Because of that, as a user, I wouldn't expect the data to appear in release builds at all, and I would find it frustrating if it did (that would be a very surprising default) or if I had to jump through hoops to avoid it.

Great! Looking forward to easier debug representation!

However, it think it would be a mistake to merge it as is - hopefully this is the first of many standard library macros - and it would be great to have a comprehensive plan for generalizing this and expanding the generated code for other things than debug description.

I’m thinking ofc of Rusts #[derive(Debug, Clone, PartialEq)] macro system.

I see an opportunity here for a similar system - with the key difference that Swift already implicitly has Rusts #[derive(Clone, Copy)] and that Swifts spelling for #[derive(PartialEq, Eq, Hash)] is simply : Equatable, Hashable {} and another important distinction is that Swift can declare the conformance to those protocols not only on the scope declaring the type, but also on an extension…

Since November last year I have been learning Rust, and now I mostly code Rust, after having coded Swift for soon 10 years. The Rust #[derive()] system is great, and I think Swift would benefit from something similar - at least the part of an grammar for declaring a list of many macros to attach to a type.

Third party developers can also declare their own macros for conformance to their traits, which go into the same derive trait (Swift: protocol) list - Rust does not discriminate standard library traits. E.g. the de facto industry standard crate (Swift: SPM target) serde has Serialize (very similar to the automagical Encodable in Swift) and Deserialize (Swift: Decodable), and they too can be included in the derive list, next to Rust standard library Debug, like so:
#[derive(Debug, Serialize, Deserialize)].

I think it would be great the spelling would be:

#[attach(DebugDescription, Foo, Bar)]
struct Organization {}

Where attach is a lets-defer-bikeshedding-of-keyword-tmp-keyword.

Personally I think it would be great if Equatable and Hashable would be forced go into thar list, because the magic that Swift has today which autosynthesises == and hash func really ought to “treated” as a macro generating code. Same thing for Codable… Codable is even more magical and strange, since eg the autosyntesised does not generate the enum CodingKeys type, but creating such an enum is the way to rename keys. It is weird, and too much opaque magic IMO. I would love for Swift 7.0 to deprecate all the autosynthesized code, replaced by macros - so that we can expand the macro and see the generated code!! (Which is great as teaching and documentation!) - no magic! And then in Swift 8.0 we can remove that magic. Potentially we would keep the possibility of using those macros on extension scopes, like we can today with extension Foo: Equatable {}.

(but perhaps upgraded?! Because today doing extension: Foo: Codable {} in a different module than the one declaring Foo does not work, maybe with a macro Codable such restriction could be lifted?)

WDYT?

1 Like

I think the name of the macro/feature is misleadingly broad. This feature is about emitting LLDB type summaries into the binary. I’d honestly prefer if the compiler just did this automatically, but a macro would be a useful stopgap.

I would suggest renaming the macro to @LLDBTypeSummary, and have it apply to the debugDescription member itself. In cases where you can’t change debugDescription, you could apply the macro to any arbitrary stored String property.

In the fullness of time, this macro would be replaced by a CustomLLDBTypeSummary protocol with an lldbTypeSummary property requirement. The compiler would synthesize the lldbTypeSummary implementation and emit the appropriate symbol.

2 Likes

This feature is about emitting LLDB type summaries into the binary. I’d honestly prefer if the compiler just did this automatically, but a macro would be a useful stopgap.

There seems to be some misunderstanding about what LLDB type summaries are that must have lead to this statement. A type summary https://lldb.llvm.org/use/variable.html#type-summary is a custom one-line summary of a complex type that displays less than what the debugger would display if it only had the debug information. For example, with the information in the debug info / reflection metadata a Swift.String would format like this:

Process 25537 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100000e39 a.out`main at t.swift:2:7
   1   	let s = "hello"
-> 2   	print(s)
Target 0: (a.out) stopped.
(lldb) v --raw s
(Swift.String) s = {
  _guts = {
    _object = {
      _countAndFlagsBits = {
        _value = 478560413032
      }
      _object = 0xe500000000000000
    }
  }
}

which — as you can convince yourself by looking at the implementation of String (swift/stdlib/public/core/String.swift at dae1e8443d48dea0bfb32ee9b8f3716f28bcdc42 · apple/swift · GitHub) — is correct, but it's also more detail than programmers are usually interested in. So LLDB has a built-in summary provider that formats the String data structure as a String literal, and leaves out all the implementation details:

(lldb) v s
(String) s = "hello"

This kind of customization is about applying judgement and domain knowledge that only the author of the data type has, and that is outside the purview of the compiler.

Perhaps I didn’t explain myself clearly: I think that if I implement CustomDebugStringConvertible with a static string backing the debugDescription property, the compiler should emit that static string as the type summary template. If I don’t want to change my debugDescription to avoid dynamic computation, I could instead conform to a new CustomLLDBTypeSummary protocol and fulfill an lldbTypeSummary: StaticString requirement.

A challenge with this approach is the compiler won't know with certainty whether the authors intend for the debugDescription to be used as a debugger type summary. This impacts delivery of diagnostics. When a type summary is not intended, users will not expect/want diagnostics. Equally importantly, when a type summary is intended, users will expect diagnostics if there are reasons why the property body cannot be converted to a type summary.

You suggest using "a static string backing" as a heuristic, but we plan to relax the criteria imposed on debugDescription bodies, to support more kinds definitions. For example string concatenation, ternary or other conditions, and hopefully more.

Alexander, I appreciate the feedback and thought you've put in. Your suggestions would be best made as their own pitch. Moving Equatable and Hashable, for example, into the macro scheme you've proposed is much beyond the scope of my proposal and focus.

How do you currently handle debugDescription and CustomDebugStringConvertible? Are they wrapped in #if DEBUG?

Regarding the name of the fallback property, maybe the name can be sidestepped by letting authors pick any name, by requiring a marker attribute/macro. Ex:

@DebugDescriptionProperty
var myDebugDescription: String {
    "here I am"
}

I haven't checked whether this can be implemented as I've sketched it, but what are folks opinion on the idea of making a tradeoff of replacing a prescriptive name with a marker macro? @ktoso @hisekaldma @Jon_Shier

Yes, since it's a protocol it's straightforward to put such conformances in a conditional extension:

#if DEBUG
extension SomeType: CustomDebugStringConvertible {
  var debugDescription: String { ... }
}
#endif

I guess since DebugDescription is a member attribute macro, does the current implementation work if someone writes it on such an extension instead?

#if DEBUG
@DebugDescription
extension SomeType: CustomDebugStringConvertible {
  var debugDescription: String { ... }
}
#endif

If so, then I think that would address my concerns about leakage/bloat.

It does work, with the caveat that it degrades the macro's ability to produce some (compile time) diagnostics. Specifically, a macro on an extension can't inspect the type's definition, which means it can't distinguish between stored and computed property references.

If that's implementable that looks fine I think :slight_smile:

After a quick prototype, it looks implementable. Now to ask the compiler folks if they see any problems with the approach.

1 Like