Level up your debugging skills with LLDB’s v, p and po commands

this article was originally posted in 2019 on my medium blog here

Debugging is an essential skill for any developer and its a wonderful thing that Apple gives us such a powerful tool in the form of LLDB. LLDB is an open source high-performance debugger that comes default in Xcode.

This article is not a deep dive into what LLDB is, but I would like to talk about three basic commands that we can use to print out variables in our source code to the debugger console in Xcode.

For this article lets assume we have this simple struct

public struct Comment{
public let id: String
public let text: String
public let date: Date
}
let comment = Comment(id: "1", text: "hello world", date: .distantPast)

po comment

For most developers , myself included, this is what I have been using for years to print out objects to the console. Sending this command will give you an output that looks like this

(lldb) po comment
▿ Comment
- id : "1"
- text : "hello world"
▿ date : 0001–01–01 00:00:00 +0000
- timeIntervalSinceReferenceDate : -63114076800.0

The nice thing about the po command is that it can also execute expressions that are valid within the scope of the breakpoint. As long as the expression is valid code that would also compile if it was in your source file. As an example

po comment.date.timeIntervalSinceNow

Will produce output on the console. For objects and expressions that are out of scope, LLDB will quickly throw an error to the console. Behind the scenes LLDB actually generates source code from your po command that is then compiled using the embedded swift and clang compilers before being executed.

p comment

This is the second way of printing out variables and is also equivalent to typing expr commentp is just an alias for expr

(lldb) p comment
(lldbDemo.Comment) $R4 = (id = "1", text = "hello world", date = 0001–12–30 00:00:00 +0000)

p prints out the object description in a similar way. While the output is equivalent, it is slightly different than that of po. If you look carefully, you can see that the resulting output above has been given a name $R4 This is part of LLDB’s convention of naming output variables with incrementing numbers. Hence p $R4.textis a valid command and will produce the expected output “hello world”

Behind the scenes, LLDB generates source code from your p command that is then compiled using the embedded swift and clang compilers before being executed. This is similar to what happens with the po command except that p performs what we call dynamic type resolution

Dynamic type resolution 🤔

This is the process of verifying the type at runtime. While referring to our comment struct above, lets consider the following example:

protocol Notification {}
public struct Comment{
public let id: String
public let text: String
public let date: Date
}
extension Comment: Notification{}
let aComment: Notification = Comment(id: "1", text: "hello world", date: .distantPast)

Notice in code the static type of aComment is Notification but at runtime the variable will have an instance type of Comment , which is its dynamic type 🤓.

p only computes the dynamic type on the result, this is so, because similar to po, it compiles and then executes the code.

So an expression such as 👇🏿 will throw an error.

(lldb) p comment
(lldbDemo.Comment) $R4 = (id = "1", text = "hello world", date = 0001–12–30 00:00:00 +0000)
(lldb) p comment.text
error: <EXPR>:3:1: error: value of type 'Notification' has no member 'text'
comment.text
^~~~~~~ ~~~~

The only way this would work is if you explicitly cast it

p (comment as! Comment).text

Now lets talk about the new way to print output to the console

v comment

The LLDB debugger has a new command alias ( since Xcode 10.2 ), v, which is an alias for “frame variable” command to print variables in the current stack frame.

(lldb) v comment
(lldbDemo.Comment) comment = (id = "1", text = "hello world", date = 0001–12–30 00:00:00 +0000)

The v command is significantly faster because it doesn’t compile and execute any code at all.

(lldb) v comment.text
(String) comment.text = "hello world"

The fact that no code is compiled means that v has a few drawbacks and a slightly different syntax which may not be the same as the language that you are debugging in. Consider if our comment struct looked like this

public struct Comment{
public let id: String
public let text: String
public var uppercasedText: String {
      return text.uppercased()
}
public let date: Date
}

Now lets look at a case where v could fail

(lldb) v comment
(lldbDemo.Comment) comment = (id = "1", text = "hello world", date = 0001–12–30 00:00:00 +0000)
(lldb) v comment.uppercasedText
error: "uppercasedText" is not a member of "(lldbDemo.Comment) comment"
(lldb) po comment.uppercasedText
"HELLO WORLD"

In the above example, notice that v failed to output the computed property comment.upperCasedTex while p and po do. This required code to be compiled and executed which the vcommand does not do.

Behind the scenes, v will examine the program state, then reads the value from memory. It then performs dynamic type resolution ( potentially multiple times ) before formatting the output and printing it to the console.

Hence for the case where we have

extension Comment: Notification{}
let aComment: Notification = Comment(id: "1", text: "hello world", date: .distantPast)

Our output will look like this





(lldb) v comment.text
(String) comment.text = "hello world"

As you can see, v just simply works. Whereas p and po will need an explicit cast. The above example works because vperforms dynamic type resolution multiple times on comment whearas po does not, and p only does it once on the final result.

Summary

The po command uses the object description, whereas the p and v commands use the data formatters to display the object on the console. A second important difference is that both poand pcompile expressions and then execute them and have access to the full language. In the case of p, dynamic type resolution is performed once on the final result.vhas its own syntax interpreters and performs dynamic type resolution for each step of the interpretation. Because vexamines the program state and then reads the value from memory, it bypasses the expression evaluator. Hencev can be a lot faster and should be preferred over p and po.