Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions stdlib/public/core/DebuggerSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,38 @@ public enum _DebuggerSupport {

return target
}

// Print an object or value without the caller having a concrete type.
//
// For simplicity of data handling in LLDB avoids using an enum return type,
// using (Bool, String) instead of Optional<String>.
@available(SwiftStdlib 6.3, *)
public static func stringForPrintObject(_ pointer: UnsafeRawPointer?, mangledTypeName: String) -> (Bool, String) {
guard let pointer = unsafe pointer else {
return (false, "invalid pointer")
}

guard let type =
unsafe _getTypeByMangledNameInContext(
mangledTypeName,
UInt(mangledTypeName.count),
genericContext: nil,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're passing nil here, does it mean this doesn't work with generic types the moment?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will work with generic types as long as the mangled name is fully bound, i.e. there aren't any "get the 3rd type parameter from the type I'm in" directives. It may not work for generic types that depend on the context (e.g. you have a [T] local variable), depending on how this mangled name is obtained. But as long as you can get a name with all the parts in it, it will work.

Copy link
Contributor Author

@kastiglione kastiglione Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as Mike says, this depends on lldb being able to construct a concrete mangled type name. @augusto2112 you've dealt the most with generic types within expression evaluation, are there known cases where this will be a problem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the generic expression evaluator can have unbound types, in fact that's essentially its whole purpose for existing, so we'd need to figure out how to pass those parameters in as well.

An easy test for you is to stop in a generic type with one generic parameter and po self:

struct A<T> {
  func f() {
    // stop here, po self
  }
}

genericArguments: nil)
else {
return (false, "type not found for mangled name: \(mangledTypeName)")
}

func loadPointer<T>(type: T.Type) -> Any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what would happen here if T is noncopyable. Presumably it would be OK not to support noncopyable types as a first implementation, but we should make sure they at least do something vaguely sensible when failing. I don't think we can use reflection on them at all, but we could potentially check for noncopyable types and return the demangled type name and some message about not being able to show the contents.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

po today doesn't work at all for noncopyable types, so this won't by any worse.

Copy link
Contributor Author

@kastiglione kastiglione Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out execution doesn't make it this far (loadPointer). Currently, _getTypeByMangledNameInContext returns nil for non-copyable types. The output is:

"invalid type 4main19StructIsNonCopyableV"

if type is AnyObject.Type {
unsafe unsafeBitCast(pointer, to: T.self)
} else {
unsafe pointer.load(as: T.self)
Comment on lines +353 to +356
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikeash following on our conversation last week, what do you think of this approach for handling objects? This is instead of providing a pointer to the object (pointer to pointer).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work, but it seems a bit overcomplicated. If you unconditionally pointer.load(as: T.self) then everything is consistent. Pass a pointer to the value, and it works.

If there's some case where lldb naturally ends up with an object pointer rather than a pointer to a value of class type, maybe that suggests a second entrypoint specifically class types?

Copy link
Contributor Author

@kastiglione kastiglione Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm torn. I can see the argument of consistency, but it also seems more natural, to me, to pass an object to the pointer argument, since it is already a pointer, rather than passing a pointer to an object. I'm also uncertain whether it will always be possible to get a pointer to an object, for example an object stored register.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that can also happen with a non-class type. A sufficiently small struct may be stored into a register. Somewhat larger ones may be stored in multiple registers. So you'll want a way to materialize a value regardless.

However, working with object pointers directly would allow you to support objects even if you don't yet have such a generalized mechanism.

Copy link
Contributor Author

@kastiglione kastiglione Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to merge this as is, because it allows forward progress, and merging does not prevent either of the following changes:

  • changing the semantics of the pointer argument (ie passing objects using a pointer-to-pointer)
  • creating two entry points, one for objects, and one for values

This function is used only by lldb and we have the freedom to alter it as needed in the coming weeks, while development and testing happens on the lldb side.

I'll also be thinking about how materialize small structs stored in registers.

thanks Mike.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

}
}

let anyValue = _openExistential(type, do: loadPointer)
return (true, stringForPrintObject(anyValue))
}
}

public func _stringForPrintObject(_ value: Any) -> String {
Expand Down
1 change: 1 addition & 0 deletions test/abi/Inputs/macOS/arm64/stdlib/baseline
Original file line number Diff line number Diff line change
Expand Up @@ -6815,6 +6815,7 @@ _$ss16TextOutputStreamPsE11_writeASCIIyySRys5UInt8VGF
_$ss16TextOutputStreamPsE5_lockyyF
_$ss16TextOutputStreamPsE7_unlockyyF
_$ss16TextOutputStreamTL
_$ss16_DebuggerSupportO20stringForPrintObject_15mangledTypeNameSb_SStSVSg_SStFZ
_$ss16_DebuggerSupportO20stringForPrintObjectySSypFZ
_$ss16_DebuggerSupportOMa
_$ss16_DebuggerSupportOMn
Expand Down
1 change: 1 addition & 0 deletions test/abi/Inputs/macOS/arm64/stdlib/baseline-asserts
Original file line number Diff line number Diff line change
Expand Up @@ -6815,6 +6815,7 @@ _$ss16TextOutputStreamPsE11_writeASCIIyySRys5UInt8VGF
_$ss16TextOutputStreamPsE5_lockyyF
_$ss16TextOutputStreamPsE7_unlockyyF
_$ss16TextOutputStreamTL
_$ss16_DebuggerSupportO20stringForPrintObject_15mangledTypeNameSb_SStSVSg_SStFZ
_$ss16_DebuggerSupportO20stringForPrintObjectySSypFZ
_$ss16_DebuggerSupportOMa
_$ss16_DebuggerSupportOMn
Expand Down
1 change: 1 addition & 0 deletions test/abi/Inputs/macOS/x86_64/stdlib/baseline
Original file line number Diff line number Diff line change
Expand Up @@ -6836,6 +6836,7 @@ _$ss16TextOutputStreamPsE11_writeASCIIyySRys5UInt8VGF
_$ss16TextOutputStreamPsE5_lockyyF
_$ss16TextOutputStreamPsE7_unlockyyF
_$ss16TextOutputStreamTL
_$ss16_DebuggerSupportO20stringForPrintObject_15mangledTypeNameSb_SStSVSg_SStFZ
_$ss16_DebuggerSupportO20stringForPrintObjectySSypFZ
_$ss16_DebuggerSupportOMa
_$ss16_DebuggerSupportOMn
Expand Down
1 change: 1 addition & 0 deletions test/abi/Inputs/macOS/x86_64/stdlib/baseline-asserts
Original file line number Diff line number Diff line change
Expand Up @@ -6836,6 +6836,7 @@ _$ss16TextOutputStreamPsE11_writeASCIIyySRys5UInt8VGF
_$ss16TextOutputStreamPsE5_lockyyF
_$ss16TextOutputStreamPsE7_unlockyyF
_$ss16TextOutputStreamTL
_$ss16_DebuggerSupportO20stringForPrintObject_15mangledTypeNameSb_SStSVSg_SStFZ
_$ss16_DebuggerSupportO20stringForPrintObjectySSypFZ
_$ss16_DebuggerSupportOMa
_$ss16_DebuggerSupportOMn
Expand Down
63 changes: 63 additions & 0 deletions test/stdlib/DebuggerSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ struct StructWithMembers {
var b = "Hello World"
}

struct StructIsNonCopyable: ~Copyable {
var a = 1
var b = "Hello World"
}

class ClassWithMembers {
var a = 1
var b = "Hello World"
Expand Down Expand Up @@ -108,6 +113,64 @@ if #available(SwiftStdlib 6.1, *) {
}
}

@available(SwiftStdlib 6.3, *)
func _expectStringForPrintObject<T>(_ pointer: UnsafePointer<T>, output: String) {
guard let mangledTypeName = _mangledTypeName(T.self) else {
expectTrue(false)
return
}
let (success, printed) =
_DebuggerSupport.stringForPrintObject(UnsafeRawPointer(pointer), mangledTypeName: mangledTypeName)
expectTrue(success)
expectEqual(printed, output)
}

if #available(SwiftStdlib 6.3, *) {
StringForPrintObjectTests.test("PointerWithMangledTypeName") {
var num = 33
_expectStringForPrintObject(&num, output: "33\n")

var val1 = StructWithMembers()
_expectStringForPrintObject(&val1, output: "▿ StructWithMembers\n - a : 1\n - b : \"Hello World\"\n")

var val2: StructWithMembers? = StructWithMembers()
_expectStringForPrintObject(&val2,
output: "▿ Optional<StructWithMembers>\n ▿ some : StructWithMembers\n - a : 1\n - b : \"Hello World\"\n")

do {
var val3 = StructIsNonCopyable()
if let mangledTypeName = _mangledTypeName(StructIsNonCopyable.self) {
withUnsafeBytes(of: &val3) { bytes in
guard let pointer = bytes.baseAddress else {
expectTrue(false)
return
}
let (success, printed) =
_DebuggerSupport.stringForPrintObject(pointer, mangledTypeName: mangledTypeName)
expectFalse(success)
expectEqual(printed, "type not found for mangled name: \(mangledTypeName)")
}
} else {
expectTrue(false)
}
}

do {
let obj = ClassWithMembers()
if let mangledTypeName = _mangledTypeName(ClassWithMembers.self) {
withExtendedLifetime(obj) { obj in
let pointer = unsafeBitCast(obj, to: UnsafeRawPointer.self)
let (success, printed) = _DebuggerSupport.stringForPrintObject(pointer, mangledTypeName: mangledTypeName)
expectTrue(success)
expectTrue(printed.hasPrefix("<ClassWithMembers: 0x"))
}
} else {
expectTrue(false)
}
}
}
}

class RefCountedObj {
var patatino : Int
init(_ p : Int) {
Expand Down