SE-0440: DebugDescription macro

Hello, Swift community!

The review of SE-0440: DebugDescription Macro begins now and runs through July 16th, 2024.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0440" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a nightly toolchain or Xcode beta and experiment with it. In the nightly toolchain, it is gated on the DebugDescriptionMacro experimental feature flag.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

What is your evaluation of the proposal?

Is the problem being addressed significant enough to warrant a change to Swift?

Does this proposal fit well with the feel and direction of Swift?

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here.

Thank you,

Steve Canon
Review Manager

10 Likes

Kicking things off myself: the feature appears to not work with generic types, producing errors like:

@__swiftmacro_13ComplexModule0A0V17_debugDescription06_DebugD8PropertyfMp_.swift:8:12: error: static stored properties not supported in generic types
static let _lldb_summary = (
~~~~~~     ^
@__swiftmacro_13ComplexModule0A0V17_debugDescription05DebugD0fMr3_.swift:1:1: note: in expansion of macro '_DebugDescriptionProperty' on property '_debugDescription' here
@_DebugDescriptionProperty("^ComplexModule[.]Complex<.+>", ["_debugDescription"])
^
@__swiftmacro_13ComplexModule0A0V17_debugDescription05DebugD0fMr3_.swift:1:1: note: in expansion of macro '_DebugDescriptionProperty' on property '_debugDescription' here
@_DebugDescriptionProperty("^ComplexModule[.]Complex<.+>", ["_debugDescription"])
^

At the least, I think that these should generate a better diagnostic when the macro is evaluated rather than failing after replacement, but this seems like something that we would like to work eventually (e.g. Complex<T> ought to be able to provide a debug summary like (\(x), \(y))--at least if T has one). Is there any path to adding support for this in the future, or would this require a fundamentally different design?

2 Likes

Huge +1, this does solve a real world problem - we were just looking at good ways to package this for our own sdk types later this year and this would be super helpful.

I've only just read the proposal, but I was wondering what would happen if the debugDescription string contained text that matched the LLDB interpolation syntax?

That is, given this debugDescription:

var debugDescription: String {
  "\(id) ${var.id}"
}

What would LLDB output?

  1. 15 15
  2. 15 ${var.id}
  3. Something else?
2 Likes

The implementation prints "15 15". Is that the intended behavior, @Dave_Lee?

The more interesting cases to consider are probably what happens with ${var.somethingThatIsntAVariableName} ("error: summary string parsing error" in LLDB), and how to escape a literal ${ if that's a desired portion of the debug description.

Yes, "15 15" is intended behavior.

The pass through of lldb syntax allows for the uncommon cases where a developer wants to use features that the macro doesn't provide a swift to lldb translation. Use of such lldb syntax is consider niche.

To escape ${, use a backslash. In a Swift string, it would be "\\${var.printLiterrally}".

1 Like

Am I correct that ${ is the only sequence that ever needs to be escaped like this?

I am not clear on how the macro interacts with CustomDebugStringConvertible.

The initial example declares both:

@DebugDescription
struct Organization: CustomDebugStringConvertible {
    var id: String
    var name: String
    var manager: Person
    // ... and more

    var debugDescription: String {
        "#\(id) \(name) (\(manager.name))"
    }
}

But the introduction says CustomDebugStringConvertible conformance is not required:

It can be used in place of CustomDebugStringConvertible conformance, or in addition to, for custom use cases.

Does that mean that using the macro will automatically add CustomDebugStringConvertible conformance?

If not, what would be custom use cases where you would want to declare both?

Also, I typically add CustomDebugStringConvertible conformance in an extension. It looks like the macro can only be applied to the type declaration itself. Is that correct?

Is it possible to add the attach the macro to an extension?

@DebugDescription
extension Organization: CustomDebugStringConvertible {
    var debugDescription: String {
        "#\(id) \(name) (\(manager.name))"
    }
}

I tried using the @DebugDescription macro with Xcode 16 beta 2 (16A5171r).

The experimental feature is enabled by default, so I'm not sure how much of this proposal can be changed.

Features.def

The "release/6.0" branch is ahead:

EXPERIMENTAL_FEATURE_EXCLUDED_FROM_MODULE_INTERFACE(DebugDescriptionMacro, true)

:warning: The "main" branch is behind:

EXPERIMENTAL_FEATURE(DebugDescriptionMacro, true)

The macro doesn't diagnose the following issues, where a description either doesn't exist or can't be found.

@DebugDescription // Doesn't add a type summary!
struct A {
  var stored: Int
}
@DebugDescription // Doesn't add a type summary!
struct B {
  var stored: Int
}

extension B: CustomDebugStringConvertible {
  var debugDescription: String { "\(stored)" }
}

The macro doesn't diagnose the following issues, where a description doesn't reference stored properties.

@DebugDescription // Adds a "${var.computed}" type summary!
struct C: CustomDebugStringConvertible {
  var stored: Int
  var debugDescription: String { "\(computed)" }
}

extension C {
  var computed: Int { 0 }
}
struct D {
  var stored: Int
  var computed: Int { 0 }
}

@DebugDescription // Adds a "${var.computed}" type summary!
extension D: CustomDebugStringConvertible {
  var debugDescription: String { "\(computed)" }
}

Diagnostic messages don't include the file:line or macro name.

<unknown>:0: error: body must consist of a single string literal
3 Likes

Yes, that's correct (however this comment identified a couple minor bugs in how lldb handles $ which I've submitted fixes for).

The macro does not automatically add the conformance. Perhaps the example should not show CustomDebugStringConvertible being used.

The @DebugDescription macro places stricter requirements on the debugDescription implementation. The macro supports basic data display. When simple formatting isn't enough, that's where CustomDebugStringConvertible provides additional value. If your debugDescription uses advanced formatting, or has to compute its results, then you'll want to declare support for CustomDebugStringConvertible.

By declaring both, you can support basic descriptions in the UI, and complex descriptions using po. If they differ, you'll need to implement both debugDescription and _debugDescription.

Another reason to support both is to ensure that po shows what the UI shows. In this case, you'll only need to implement debugDescription.

The macro works for extensions too. However when applied to an extension, the macro has less diagnostic ability (since it can only see the extension, and not the original declaration).

1 Like

Yes, the macro can be applied to an extension. When applied to an extension, the macro has less diagnostic ability (since it can only see the extension, and not the original declaration).

Thanks for the effort and the proposal!

Agree on this, seems the current example is only showing the macro usage with the specific protocol.

Would be great if there are other examples that is not using CustomDebugStringConvertible !

Apart from that, if we are not creating customized debug descriptions, will there be any default formats from the Macro, that allows to interpolate with LLDB?

Would it be simpler if the user could directly attach the macro to a property? Then instead of a fixed search order (_debugDescription, debugDescription, description) any property with a suitable body could be used.

extension A {
  @DebugDescription
  private var _summary: String { "\(stored)" }
}

The enclosing type name (but perhaps not the stored or computed properties) should be available from the lexicalContext.

1 Like

By attaching to the type, the macro can, in some cases, diagnose use of computed properties. If the macro were instead attached the property, it would not be possible to report to the user that format will fail at debug time.

The idea has been suggested to use a macro to let developers select which property is used. In fact, the current implementation of @DebugDescription does this on behalf of the user, by emitting the "internal" macro @_DebugDescriptionProperty onto the selected property.

Personally, I am reluctant to remove the ability to diagnose computed properties in favor of the ergonomic improvements of attaching the macro to the property instead of the type.

A compromise solution is to allow an optional macro which developers can use to select a property of their choice, but this has the downside of using two macros (one one the type, one on the property). I am not opposed to this though.

Hypothetically, if macro expansion were provided property-level information about the type definition (for example via the lexicalContext, or similar), then yes it would be ideal to have a macro on the property itself.

1 Like

Thanks for this report.

I hadn't thought of this case, where a computed property is referenced from in a type declaration "before" it's defined in an extension. I believe this can addressed with a change to the macro implementation.

This is expected. When attached to an extension, the macro system (currently) provides no visibility into the original type definition. Without the syntax nodes of the type definition, it's unknown to the macro whether a property is computed or stored. If a computed property is referenced in this case, lldb will produce a diagnostic in the console. Maybe in the future the macro system will have an affordance to allow introspection of a type's definition from an extension.

Thanks, I'll look into this.

1 Like

Can you clarify what you mean by default formats?

FWIW, there's some precedent here with macros like those in Observation's module, where @Observable goes on the type and then @ObservationIgnored is attached to individual properties (and there's also the @ObservationTracked property macro that's added internally by @Observable, like you're doing with @_DebugDescriptionProperty).

So maybe making @_DebugDescriptionProperty public and automatically applied unless the user places it somewhere explicitly is a clearer path forward?

3 Likes

I feel like only offering the the property macro is more future proof. If macros ever get the ability to walk the enclosing context, we can add retroactively add the diagnostics. This also reads much better – it's the property that's the "debug description", not the type.

If we choose to go with the macro on the type for the improved diagnostics in the short term, then we shouldn't include support for applying it to extensions for the same reasoning.

2 Likes