DWIM Print Command

This is a proposal to establish a new print command whose primary purpose is to choose how to print a result, using one of the existing commands. This can be thought of as a print abstraction on top of the multiple existing “print” implementations.

LLDB has multiple commands to print the process’s data, including:

  • expression (aliases p and po)
  • frame variable (alias v)
  • register read
  • memory read (alias x)

The majority of users use p/po, because it’s widely known and in many cases p works as an alternative to the other print commands. Although its primary purpose is to evaluate source language statements, it can also be used as a stand in for other commands that print. For example:

  • p someVar instead of v someVar
  • p $reg instead of register read reg
  • parray 4 (uint32_t*)someVar instead of x/4wu someVar

The last one is a contrived example, the point is that people are more familiar with the source language than the debugger, and will use it because of that familiarity.

In fact, expression evaluation can go even further. In ObjC, po 0x12345670 can be used to print an object by its address, providing what is essentially an LLDB specific form of dynamic typing. There are a number of features within and around expression evaluation.

However there are some downsides to using p as a universal print command. Some issues with expression evaluation are:

  • the implementation is large and complex, and as a result it has more failure points, and it can be slow
  • there can be unwanted side effects
  • the source language syntax and semantics can impose limits/burdens on data inspection

To the first point, consider lldb-eval, which supports a syntax that falls between frame variable and expression to provide a middle ground between the performance and reliability between those two commands.

This proposal assumes readers agree that expression evaluation can be fragile, slower, or both. The document focuses on the other issues, as they represent divergent behavior between existing expression and the other printing commands (specifically frame variable).

For performance, reliability, and functionality, users have been advised to print variables with v rather than with p.

Advising users isn’t always enough. Not all users have heard this advice. Many users don’t know that v and other print commands exist. Once they do know, they also need to know when to use the other commands and when not to. Many users don’t have this knowledge. In fact, many users don’t want to have to think about these debugger-centric details. Some users don’t want to learn distinctions that the debugger cares about – distinctions they may find unimportant.

Do What I Mean (DWIM)

From DWIM on Wikipedia:

attempt to anticipate what users intend to do, correcting trivial errors automatically rather than blindly executing users’ explicit but potentially incorrect input.

Some people, after learning about v, will ask “Can we have a command that chooses a preferred method to print?” Many users see the p command is an abstraction for printing, but to lldb it’s not that, it’s a command which performs in-process expression evaluation – which prints data as one of its effects.

Users don’t always care about the means, just the end result. If we view the existing commands as building blocks, as different implementation of a “print interface”, then we could imagine a DWIM print command that, based on its input and context, determines which printing implementation to use.

For the rest of the document, imagine a new DWIM print command whose job is to print data, without being tied to a specific printing implementation. For one invocation it may use expression, and for another invocation at might use a different command, such as frame variable.

Simple Cases

The simplest example is printing a local variable, which one can do by running either p localVar or v localVar. There’s almost no syntax here, only a token. The printed output should be identical in all cases. To implement print localVar, the logic can be illustrated with this Python function:

def dwim_print(frame: lldb.SBFrame, user_input: str) -> None:
    value = frame.FindVariable(user_input)
    if not value:
        value = frame.EvaluateExpression(user_input)
    print(value)

FindVariable first looks for the variable (from debug info). If the variable doesn’t exist, then expression evaluation is used instead.

This base logic can easily be extended to support registers:

def dwim_print(frame: lldb.SBFrame, user_input: str) -> None:
    value = frame.FindVariable(user_input)
    if not value:
        value = frame.FindRegister(user_input)
    if not value:
        value = frame.EvaluateExpression(user_input)
    print(value)

The command print pc would print the value of $pc . With this implementation, if a variable named pc exists, that would be used instead. An alternative implementation could print both values, in the rare (unless you work on a debugger) case where both a variable and register exists by the same name.

If this was all there was to the story, then it would be straightforward to add a DWIM print command that supports variables, registers, and expressions. However the complexity goes deeper and there are more cases to consider.

Effects of expression

Even with the simple case of evaluating an expression consisting of an only a plain variable, there are side effects to be aware of. The p command creates “persistent results” – variables named $0, $1, $2, etc. These variables are snapshots of the expression’s result, and effectively have global scope. Persistent results are always created as part of expression evaluation, there is no way to opt out. When using p, lldb prints the persistent variable name, but with po, the result variable name is not included in the output. To demonstrate:

(lldb) p obj
(NSObject *) $0 = 0x0000600000008000
(lldb) po obj
<NSObject: 0x600000008000>

(lldb) p obj
(NSObject *) $2 = 0x0000600000008000
(lldb) p $1
(NSObject *) $0 = 0x0000600000008000

Even though the po obj command didn’t mention $1, the persistent result was in fact created. Users who primarily use po may not even realize the persistent results exist.

We can see now the first way in which p obj and v obj differ. If a DWIM print invocation chooses to use v, there will be no persistent result. This is an issue if the user wants to make use of the persistent result. In my experience, most users do not use persistent result variables. (This is especially true for users who predominantly use po and don’t even see the persistent variable name). Additionally, a user printing a variable generally don’t need a second variable for it.

Some use cases of persistent variables are:

  • Referring to data after its variable goes out of scope
  • As persistent results are a snapshot, they can be used to compare data temporally, between a current value and a previously persisted value
  • Composing p commands by passing the persistent result of one expression, as an argument into another expression

Considering all of the above, it seems reasonable for a DWIM print command to vary in whether or not it creates a persistent result. Advanced use cases that require persistent results can still use p/expression directly.

Persistent Results: Memory Tradeoffs

Making persistent results opt-in has an additional benefit: it avoids challenges around memory references. Today, persistent values can result in the user dealing with one or more of: unsafe memory, violated semantics, or unexpected retains/ownership.

In C, a pointers captured by a persistent result are inherently unsafe. When using those variables, the users won’t know if such pointers are still valid.

In ObjC, pointers within persistent variables could be retained (using ARC) to enforce validity. But LLDB does not do that. Remember that persistent results are global, and if these variables were retained, the memory would never be freed. This could be an inconsequential memory leak, a large leak, or it could affect program behavior, which can even induce new bugs.

In C++, has the same issues with raw pointers as C does. But, C++ has smart pointers. If you were to guess, what are semantics of a persistent result whose type is std::shared_ptr<T>? Does the persistent result variable retain the pointer? If you guess yes, then you may be surprised that it does not. The persistent result is a raw data snapshot, and the smart pointer semantics are not adhered to. The alternative would be to preserve the semantics and retain the pointer, but as with ObjC, this could be a simple memory leak, but it could have worse side effects or introduce bugs.

In Swift, the language’s semantics are preserved. A persistent result variable is defined using let, and this results in pointers being retained. In Swift, po obj can cause memory leaks, or possibly worse.

No matter which choice is made, there are downsides. By making persistent results opt-in, this entire issue can be avoided. If persistent results had non-global scope, such as function/frame scope, this wouldn’t be as much of an issue, but changing scope would limit some of the use cases of persistent variables.

Now that persistent results have been discussed, let’s switch to another difference between p and v.

Syntax and Semantics

Following local variables, the next common example to consider is printing fields or member variables. Using C++ as an example, the comparison of p memberVar and v memberVar matches a lot of what has been said above about local variables, and about persistent results. Except what these commands are really doing is p this->memberVar and v this->memberVar. This introduces some syntax, the arrow operator ->. To support this, the pseudocode could be changed to:

def dwim_print(frame: lldb.SBFrame, user_input: str) -> None:
    value = frame.GetValueForVariablePath(user_input)
    if not value:
        value = frame.FindRegister(user_input)
    if not value:
        value = frame.EvaluateExpression(user_input)

    print(value)

The change is to use GetValueForVariablePath instead of FindVariable. This introduces the concept of variable path expressions. Variable path expressions have the following operators:

  • member of pointer (->)
  • member of object (.)
  • pointer dereference (*)
  • array (and pointer) subscript ([])
  • address-of (&)

These operators allow expressions to perform some of the most common data traversal operations, and conveniently, but not necessarily by design, they happen to be mostly a subset of such operations in C/C++/ObjC. However, it’s not a strict subset, of syntax or semantics, which raises some issues.

A DWIM print command would receive at least two kinds of syntax, the full syntax of the source language, and the limited syntax of variable paths. Two syntaxes complicate matters. At this juncture, some questions arise:

  1. For these operators, are the semantics the same between variable paths and expressions?
  2. What is the future of for variable path expressions? Will it evolve to add more syntax/features?
  3. How do variable paths integrate with other language syntaxes? Should each language be able to provide its own variable path syntax?

Of these, the biggest topic to discuss is semantics. A DWIM print command must be aware of semantic discrepancies when choosing how to evaluate a given expression.

Semantics

This is a multipart discussion.

  1. Operator Overloading

In C++, with the exception of ., the operators used in variable paths can be overloaded. What this means is p a->b (for example) could run arbitrary code, while v a->b would perform direct data access. If it’s known there’s no b field, then the only possible option is expression evaluation. If it’s known there’s no operator-> overload available for the type of a, then expression evaluation isn’t needed. A DWIM print command could use variables when it can determine that it’s safe to do so, and expression evaluation in other cases. This means LLDB needs to analyze the expression, in order to decide whether a variable path evaluation can be used. But before discussing analysis, let’s segue into another aspect of semantics.

  1. Synthetic Children

For display purposes, LLDB allows data formatters to define synthetic children for data types. This is a crucial feature for debugging, allowing LLDB to support data abstraction, and not burden the programmer with the raw implementation details of every type. Since LLDB shows the user a synthetic structure, it would be weird to not allow the user reference that structure. As a result, variable path expressions support synthetic children. This is in contrast with the source language, where the debugger’s synthetic view does not exist. A DWIM print command can support synthetic children, but there are cases where there’s a conflict in semantics. The first is the -> arrow operator, which synthetic children can also override. The second conflict can happen with the [] array subscript, which for variable paths uses only numeric indexes. Consider std::map<int, T> aMap, the expression aMap[1] means different things when treated as a variable path vs as a source language expression. As a variable path, it refers to the second child (whatever that may be), and in C++ it means the value for the key 1. A DWIM print command has to have logic to handle these and any other edge cases.

  1. Dynamic Typing

Variable paths have an invaluable feature over expressions, they operate on the dynamic types of objects. That is, v a->b will use the dynamic type of a to find the b member. This works even if the static type of a has no b field. The dynamic type could be a subclass, or an implementation of a protocol. In essence, variable paths perform automatic/implicit downcasting. Since a debugger’s purpose is to inspect a running program, full visibility of types and data at runtime can be indispensable. However this dynamic behavior diverges from the source language, where static types (naturally) dictate data access.

Ideally, a DWIM print command would behave consistently. If a DWIM print sometimes uses dynamic typing, and sometimes does not, then users could conclude the command is buggy and not use it.

What’s needed is an evaluation mode that supports dynamic typing even when falling back to expression evaluation.

As an aside, this mode of evaluation would solve an all too common issue that arises when users are advised to use v instead of p: handling of properties. In ObjC and Swift (as well as D, C#, numerous scripting languages and yes even C++) a property is a field that syntactically looks like a plain data member, but is backed by getter/setter functions. Currently with ObjC and Swift, the following code forces lldb users to use p:

// ObjC
@interface MyThing : NSObject
@property (nonatomic) int number;
@end

@interface MyClass @end
// Swift
class MyThing {
  var number: Int {
    return /* some computed */
  }
}

In both cases, the following command will fail:

(lldb) v thing.number

This failure is despite the expression being a valid expression. This is a cognitive burden on the user, requiring them to know whether the particular property they want to inspect needs p or whether v can be used.

Expression Evaluation with Dynamic Types

This section provides an rough answer to the question: How can LLDB support dynamic typing in expression evaluation?

Let’s start with this common command:

(lldb) p object

While printing the result of the expression, LLDB attempts to determine the dynamic (i.e. runtime) type of object. For example the exact subclass. If this succeeds, LLDB is able to print more data about the object – the fields of the dynamic type. This is a valuable debugging feature – during a debugging session you want all the state and execution information available, to help understand bugs. Using only the static type information is a limitation.

The use of dynamic typing is available for any expression result, not just for variables as the above shows. Dynamic typing works here too:

(lldb) p func()

The dynamic typing occurs on the expression result, not any of the input’s subexpressions. In other words dynamic typing is done after a result has been returned, not before or in the middle of an expression.

However, once LLDB has shown the user that it knows the dynamic type of a variable, the user might reasonably expect to be able to perform operations that depend on that type, such as:

(lldb) p object.subclassOperation()

In this example, the function subclassOperation represents a function declared on the dynamic type, not on the static type. Expression evaluation will fail on this input, as the compiler doesn’t have the information the debugger has, the concrete type of object. The compiler only has its static type. Users can work around this by changing their expression to include casts, for example:

(lldb) p ((Subclass&)object).subclassOperation()

However, this is clumsy and having LLDB seeming alternate between aware of dynamic types and unaware, is not a user-friendly workflow.

In addition to expression evaluation, LLDB has another kind of expression evaluation: frame variable. These expressions can determine the types of variables, and their members (children). This is implemented using memory reads and type metadata, which are operations that are faster and more resilient for accessing data, compared to using full expression evaluation, which is slower and more fragile.

Note: For the purpose of demonstration, assume we have two types: a base class which has a smaller interface, and a subclass which has additional member data and/or functions:

class Base {
public:
  virtual ~Base();
  int baseData;
  void baseFunc();
};

class Sub : public Base {
public:
  int subData;  
  void subFunc();
};

Let’s do a quick comparison between frame variable and expression. Assume there exists a variable named object, declared as Base &, but whose runtime type is Sub &.

(lldb) p object.subData
(lldb) v object.subData
  • The p command will fail, as there exists no field subData on Base
  • The v command will succeed – object is determined to be an instance of Sub, and its subData field is printed

This behavior is limited to expressions that frame variable supports, namely direct data access, from variables down into arbitrarily nested data members, and does not include function calls. Thus, neither of the following will work:

(lldb) p object.subFunc()
(lldb) v object.subFunc()
  • The p command fails because subFunc does not exist on the Base type
  • The v command fails because its limited syntax doesn’t support function calls

Proposed Implementation

For consistency and for an improved user experience, LLDB could provide a high level expression evaluation that is a hybrid of frame variable and expression. This high level evaluation combines the dynamic typing of frame variable with the source language support of expression evaluation, to allow users work seamlessly with runtime data.

To implement this, LLDB could automatically rewrite expressions to leverage valid frame variable subexpressions within them. There are two ways to achieve this:

  1. Rewrite expressions using casts
  2. Materialize persistent results and use those

In both cases, LLDB will need to perform an initial pass over the expression, to identify and evaluate valid frame variable subexpressions.

To identify which parts of an expression are valid frame variable subexpressions, LLDB will use the parsers of its embedded compilers (Clang, etc). Let’s start with the first expression we looked at:

(lldb) p object

The Clang AST for this expression is simple.

DeclRefExpr <line:1:1> 'Base':'Base' lvalue Var 'object' 'Base &'

This first expression demonstrates that variable access begins with a DeclRefExpr node.

Note: The AST dumps shown in this document have been reduced for readability. For example runtime memory addresses, file paths, and implicit cast nodes have been removed.

Next up consider an expression that accesses a data member on object:

(lldb) p object.baseData

For this expression, the Clang AST is:

MemberExpr <line:1:1, col:8> 'int' lvalue .baseData
`-DeclRefExpr <col:1> 'Base':'Base' lvalue Var 'object' 'Base &'

A new AST node is introduced, MemberExpr. This node is used for both . and -> member access. This AST node is used for each step of data access – an expression like a.b.c will be represented in AST form as:

MemberExpr ...
`- MemberExpr ...
  `- DeclRefExpr ...

In other words, frame variable expressions using . and -> will be represented as a leaf DeclRefExpr node, with zero or more MemberExpr upward nodes, one for each member access.

Another way of representing this is: DeclRefExpr > MemberExpr

However this considers only the static case, which isn’t interesting as it does not require special handling. Let’s next look at expressions that require dynamic typing to succeed. Let’s see what the AST looks like for this expression:

(lldb) p object.subData

The corresponding AST is:

RecoveryExpr <line:1:1, col:8> '<dependent type>' contains-errors lvalue
`-DeclRefExpr <col:1> 'Base':'Base' lvalue Var 'object' 'Base &'

This shows that Clang’s AST is not limited to syntactic information, but semantic information as well. Here we see that accessing subData from a base class instance results in an AST containing a RecoveryExpr node.

Let’s compare the the difference between a statically valid expression (object.baseData) and a dynamically valid (but statically invalid) expression (object.subData).

Both contain the same DeclRefExpr leaf node, but the parent node differs in type: MemberExpr vs RecoveryExpr. While the node type differs, the source location information is identical. In the dynamic case, the RecoveryExpr node identifies the point, the immediate predecessor, at which there’s a dynamic type issue to resolve. The predecessor of the RecoveryExpr is the DeclRefExpr, and that is the subexpression for which we need to determine the dynamic type. In this case, the subexpression is “object”.

This structure is not limited to one level of nesting. Imagine our object is nested in some outer object (“outer”), then the expression outer.object.subData will have an AST that looks like:

`-RecoveryExpr <line:20:1, col:14> '<dependent type>' contains-errors lvalue
  `-MemberExpr <col:1, col:7> 'Base' lvalue .object
    `-DeclRefExpr <col:1> 'Outer' lvalue Var 'outer' 'Outer'

In this case, the predecessor to RecoveryExpr is the sequence of DeclRefExpr and MemberExpr that corresponds to outer.object. The dynamic type of outer is not needed, while the dynamic type of object is.

With this information, we can pinpoint where LLDB needs to provide dynamic type information. The RecoveryExpr nodes represent seams that stitch together frame variable expressions and source language expressions. With this knowledge, we can construct a high level AST where the nodes represent coarse grained subexpressions of either type. The subexpressions nodes will be processed by either frame variable or expression. For illustration, the high level AST for object.subFunc() would be:

ExpressionNode '.subFunc()'
`- FrameVariableNode 'object'

As mentioned previously, to glue the two types of expressions, LLDB can rewrite subexpressions with downcasts introduced, or by replacing subexpressions with persistent result variables. Evaluation of this high level AST can be implemented by evaluating nodes bottom up, following data dependency order.

Dynamic Typing On-Demand

The expressions shown thus far have been cases where the dynamic type is needed, otherwise evaluation will fail. Applying dynamic typing on demand has obvious and non-obvious benefits:

  1. There’s no reason to determine dynamic typing where it isn’t actually needed
  2. Using dynamic typing can create confusing and unwanted changes to language semantics

The first should go without saying. The second is an interesting point to discuss. Consider this command:

(lldb) p f(object)

In languages that have function overloading, there can be more than one viable function f to call, for example there might be both f(Base &) and f(Sub &). If dynamic typing is unconditionally applied to object (ie even when it’s not required), then our expression evaluator could unintentionally introduce multiple dispatch (aka multimethods) to the target language. Changing semantics of this kind is not a goal, and would create confusion to users. The proposed algorithm does not allow multiple dispatch to happen, since dynamic typing is performed only where static typing is insufficient. Users who want to control function selection can use explicit casts in their expression.

Dynamic typing can be limited to on-demand by applying only to AST subtrees that have a path matching this pattern:

DeclRefExpr > MemberExpr* > RecoveryExpr

Iterative Expression Evaluation

Thus far we’ve considered cases where dynamic typing is applied to data access chains. Next, we’ll want to consider dynamic typing the result of expression evaluation. Again, starting from the basic case, consider this print command:

(lldb) p f()

If the result type of f() is Base & (and thus can be dynamic) then LLDB will determine the dynamic type. The user could reasonably expect to make use of that dynamic type and run:

(lldb) p f().subFunc()

The member function subFunc() may depend on the dynamic type of f(). As we’ve seen, this can determined by parsing the expression where once again a RecoveryExpr node will indicate that dynamic typing is necessary.

When dynamic typing is necessary, multiple expression evaluations will be required. First the subexpression f() is evaluated, and then, the remaining subexpression, .subFunc(), can be evaluated. There are two ways to construct the second expression:

  • Using persistent results, ex: $0.subFunc()
  • Adding casts the the original expression, ex: static_cast<Sub&>(f()).subFunc()

While the second is possible, it has potential issues. Repeated calls to f() could have the following problems:

  • Side effects
  • Slower perceived performance of LLDB
  • No guarantee the subexpression is deterministic
    • The return value could differ the second evaluation
    • The return value and return type could differ in the second evaluation, resulting in an invalid cast

For these reasons, it seems that persistent results are the preferred way to implement this.

With either method, for this two part example, the sequence of steps will be:

  1. Evaluate the first expression, f()
  2. Determine the dynamic type of the expression’s result
  3. Evaluate <rewritten>.subFunc()

Where <rewritten> is one of the two substitution options, likely to be $N.

This example shows two expression evaluations, but an input expression could require more. The evaluation process can be reduced to a value done in a loop, until the expression is fully consumed.

Let’s look at the AST to see how we can identify dynamic subexpressions indicate whether the expression is statically valid or not. Here are the ASTs for the static (first) and dynamic (second) cases:

`-CXXMemberCallExpr <line:1:1, col:13> 'void'
  `-MemberExpr <col:1, col:5> '<bound member function type>' .subFunc
    `-CallExpr <col:1, col:3> 'Sub':'Sub' lvalue
      `-DeclRefExpr <col:1> 'Sub &()' lvalue Function 'f' 'Sub &()'
`-CallExpr <line:1:1, col:13> '<dependent type>' contains-errors
  `-RecoveryExpr <col:1, col:5> '<dependent type>' contains-errors lvalue
    `-CallExpr <col:1, col:3> 'Base':'Base' lvalue
      `-DeclRefExpr <col:1> 'Base &()' lvalue Function 'f' 'Base &()'

In the first case, the expression can be evaluated as-is. In the second case, expression evaluation will need to evaluate each path from a leaf node to a recovery node. In this case, the [leaf,recovery) path has a range of 1 to 3, and corresponds to f(). The remaining part of the expression, .subFunc() will be evaluated in a second expression. The second expression will depend on the persistent result of the first expression. The second expression could be invalid, but that can only be determined after the first expression.

Non-goals of Dynamic Typing

Thus far we’ve looked at single expressions. What about complex expressions, those with multiple statements or with control flow? Should a DWIM print command provide dynamic typing for such expressions? Let’s look at one:

(lldb) p for (const Base &x : vec) printf("%d", x.subData);

What does it mean to print a for-loop? Or an if-statement? In addition to these hard to answer semantic questions, to implement dynamic typing within control flow statements would require more complicated evaluation logic. It’s a slippery slope towards evaluating more and more C/C++. For example, in this for-loop, the evaluator would have to perform the iteration itself, in order to dynamically type the loop variable x.

This new DWIM print command would provide dynamic typing only for single expressions that can be evaluated in a linear unconditional order. These are the expressions where dynamic typing is desirable. Expression that contain control flow, multiple statements, and closures (to name a few), will not support dynamic typing. By limiting the scope of where dynamic types are employed, the mental model should be reasonable for users to understand, hopefully fairly intuitive.

Miscellaneous

Variable Path Syntax

While variable path syntax is a subset of C/C++/ObjC, that is not true for other languages. Swift, for example, has the . operator but not the -> operator. It has other relevant operators, such as ? and !. For this reason, it might be good to allow language plugins to define their own variable path syntax, to ensure an ideal amount of overlap exists between source language and variable paths.

Transparency

To prevent any misunderstanding between how the user expects evaluation to be done, and how it is done, the DWIM print command could optionally print a command the represents the most direct “low level” way to print the value. For example, if the DWIM print sees that dynamic typing required for the expression to work. Such as:

(lldb) v a->b
note: equivalent command: p ((Subclass *)a)->b

This addresses the case where users want to paste an expression they’ve used into the source and expect it to compile. This won’t always be possible, for example if the expression references registers or persistent results, but that’s also the case today with p. These can be thought of as an lldb form of fix-its.

Another benefit to printing equivalent low level commands is educating by showing them other commands, commands they may want to explore and use themselves.

Conclusion

The primary goal of this proposal is to provide a single print command, which chooses the most reliable, performant, and dynamic method of printing. At its most distilled, the goal is for DWIM print anyVar to use frame variable, and DWIM print someExpr() to use expression. In considering each of these, and their differences, there are a number of emergent cases to handle, including:

  • persistent results
  • syntax/semantic differences between frame variable and expression
  • dynamic typing

Hopefully this proposal has laid out most of the details needed to assess how these aspects can be handled by a DWIM print command.

Thank you for reading, all feedback is appreciated!

2 Likes

On a high level, I have two main concerns with this. The first is the introduction of another expression-like command and its associated syntax. I think we agree that the currents situation where one has to choose between two “expression” commands (“expr” and “frame var”) is annoying for the users, but I dread the idea of having to explain the differences between three of them. I know this is supposed to be like “one command to rule them all”, but I think that an equally likely result is something like

where we have three expression-like commands, which all superficially look very similar, but differ in various corner cases, and neither of them is a superset of the other.

The second one is the performance of the new command. Based on your description of the proposed implementation, I get the impression that you still want to put the user-provided expression through the regular expression parser (but skip the compilation step). Now, I don’t have any good data on this (I guess I should get some), but anecdotally, the most expensive part of the expression evaluation (for our use cases at least) is the parsing of the expression, as that’s where clang makes zillions of queries about different entities, and lldb has to parse lots of debug info to answer them. Of course, sometimes all that information is really necessary to resolve the expression, but often it isn’t. The most pathological case for that is an expression like “1+1”, which requires zero context, but it will still cause lldb to parse the this object (and its transitive closure), just in case it really is needed. If this happens to be some huge “world” object, then this expression can take quite some time.

With that in mind, I’m wondering if it wouldn’t be a better approach to handroll a parser for the these DWIM expressions. I know that the thought of parsing c++ seems daunting, but I think that the very limited subset (basically, ., -> and []) of operations would make this achievable.

I definitely wouldn’t want to be parsing expressions like foo(bar, baz). ADL and function overloading would make that a nightmare, and any (correct) implementation of that would likely end up pulling all the information that clang pulls anyway. However, conveniently for us, these don’t play any role in the operations above (ok, [] can be overloaded, but I think that could be managed because the operator only takes one operand, and it is something that might need to special-case anyway, as that syntax is shared with frame var expressions).

As for the command proliferation. I’d very much consider replacing the frame var command with these new expressions – instead of rolling out a separate command. If it turns out to be successful, and powerful enough, we could still alias it to the p command, but there would still be only two overall syntaxes to think about. (Okay, maybe two and a half, because we’d probably also want a version of this that guarantees to not resume the target – but that could still be done using the same underlying parser/engine, but have it reject some constructs).

There’s also a very tricky (but not all too uncommon, particularly in some recursive template patterns), case where the subclass has a member which shadows a member of the base, and so object.data is always valid, but it’s meaning depends on whether you use the dynamic or static type for the lookup. In situations like this “frame var” and “expr” disagree on the result.

| labath
October 20 |

  • | - |

On a high level, I have two main concerns with this. The first is the introduction of another expression-like command and its associated syntax. I think we agree that the currents situation where one has to choose between two “expression” commands (“expr” and “frame var”) is annoying for the users, but I dread the idea of having to explain the differences between three of them. I know this is supposed to be like “one command to rule them all”, but I think that an equally likely result is something like

where we have three expression-like commands, which all superficially look very similar, but differ in various corner cases, and neither of them is a superset of the other.

The second one is the performance of the new command. Based on your description of the proposed implementation, I get the impression that you still want to put the user-provided expression through the regular expression parser (but skip the compilation step). Now, I don’t have any good data on this (I guess I should get some), but anecdotally, the most expensive part of the expression evaluation (for our use cases at least) is the parsing of the expression, as that’s where clang makes zillions of queries about different entities, and lldb has to parse lots of debug info to answer them. Of course, sometimes all that information is really necessary to resolve the expression, but often it isn’t. The most pathological case for that is an expression like “1+1”, which requires zero context, but it will still cause lldb to parse the this object (and its transitive closure), just in case it really is needed. If this happens to be some huge “world” object, then this expression can take quite some time.

With that in mind, I’m wondering if it wouldn’t be a better approach to handroll a parser for the these DWIM expressions. I know that the thought of parsing c++ seems daunting, but I think that the very limited subset (basically, ., -> and []) of operations would make this achievable.

I definitely wouldn’t want to be parsing expressions like foo(bar, baz). ADL and function overloading would make that a nightmare, and any (correct) implementation of that would likely end up pulling all the information that clang pulls anyway. However, conveniently for us, these don’t play any role in the operations above (ok, [] can be overloaded, but I think that could be managed because the operator only takes one operand, and it is something that might need to special-case anyway, as that syntax is shared with frame var expressions).

As for the command proliferation. I’d very much consider replacing the frame var command with these new expressions – instead of rolling out a separate command. If it turns out to be successful, and powerful enough, we could still alias it to the p command, but there would still be only two overall syntaxes to think about. (Okay, maybe two and a half, because we’d probably also want a version of this that guarantees to not resume the target – but that could still be done using the same underlying parser/engine, but have it reject some constructs).

I’m not sure we can supplant frame var with the DWIM printing command. Sometimes you need to access both the “data inspection” view of a variable path and the “expression” view at the same time. This is most obviously the case with → operators. For instance, consider some kind of access-verification smart pointer that keeps some side-data and if the pointer when accessed violates a predicate using that data, the → operator function crashes. You very much want to be able to ask “will this dereference crash” and you very much want to see the dereferenced contents so you can tell why it unexpectedly crashed. The former requires the expression evaluator, the latter is a frame var task. So I don’t think the thing the multiplexes between these two tasks can be one or the other of these printing mechanisms.

JIm

What if the new frame var/dwim command always preferentially used the “safe” version of operator-> (as provided by the data formatter), and only fell back to the “unsafe” version if that wasn’t available (and running code was enabled). That way, the “frame var” could still serve as the “data inspection view”, and “expression” would serve the “expression view”.

The multiplexing part is precisely the thing I want to avoid, because I fear it will lead to surprising behavior if some string is valid in both view but has different interpretations, and the multiplexer sometimes ends up choosing one and sometimes the other.

This DWIM print itself wouldn’t introduce new syntax. Moving on to the other part of your concern…

One topic I intentionally didn’t include in the proposal (to not distract from the core idea), is that we could reassign the p alias to the DWIM print command. Even if this doesn’t happen in upstream llvm, vendors and users are each free to do so. I mention this now because it is precisely the users who use p for everything that the DWIM print command is intended for. These are users who want one print command, and don’t care about how it gets done. These are users we won’t have to “explain the differences to”, because they don’t care :slight_smile:.

Users who know about v and p can continue to use them and ignore DWIM print. In my experience, this is a relatively small number of users, and even among these users they don’t always pick the right command. For example, I mentioned properties in the proposal (ObjC, Swift and possible but uncommon in C++). A user might use v obj.prop thinking it’s some data field, only to learn that property is actually backed by a function. Users who know the difference might appreciate the convenience of not having to look up how a property is implemented before printing it.

Having said all that, my first response when I read your reply is that I definitely wouldn’t want to contribute an “N+1th standard” sequence, that’d be no good. My first thought was:

Are there future “print” commands that we can foresee? If so what are they?

I haven’t used every debugger out there, but in my experience most debuggers have one or maybe two print commands. The evidence from other debuggers makes me think there aren’t too many additional print commands that lldb might add, which makes me less concerned about the risk.

But, assuming there are additional print commands to be added, I see two possible situations:

  1. They are complementary to existing print commands, which means the DWIM command can be updated to make use of the new print command
  2. There is some sort of conflict that prevents DWIM print from supporting the new print command

In the first case sounds ideal, and not a problem. The second case would be a problem. So my question is, can we foresee any such commands that wouldn’t work with the DWIM print approach? If so, I am eager to discuss those.

As you mention, there is the possibility of advancing existing print commands, such as expanding the syntax/features of frame variable. I think that discussion is great, but an improved frame variable is beneficial to a DWIM print command, not mutually exclusive to it.

The user expression would not always be parsed.

If the expression can be parsed as a frame variable expression, then it can avoid invoking a the compiler’s parsing.

In some cases, an expression may be a valid frame variable syntax, but the DWIM command might still choose to expression evaluation instead. Such as if there’s an operator-> overload on one of the types.

I don’t think you can dismiss that easily. By “syntax”, I meant the syntax (and semantics, I guess) of the expressions that the command accepts. There are definitely differences there – the best you can hope for is that the “DWIM” command syntax will be some sort of a “union” of the other two. I am doubtful it will be that clean, though it’s somewhat hard to say what will the problems be, as I am not really sure what the proposed implementation is. If the implementation will be to first try to evaluate the expression as “frame var”, and then (in case it fails) try to run it through the expression parser (with the fixups that are mentioned in the OP), then the following can happen.

Assume code like this:

struct Base { int x; };
struct WithGetter { int get(); };
struct Derived : Base { WithGetter x; };
Base &obj = ...;

and lldb session could look like this:

(lldb) dwim-p obj
(Derived &) obj = ... # okay
(lldb) dwim-p obj.x
(WithGetter) obj.x = ... # so far, so good
(lldb) dwim-p obj.x.get()
error: "int" has no member "get"
(lldb) go home you're drunk

It’s possible one could come up with some rules/heuristics to fix this particular problem, but I don’t think one could guarantee a consistent and composable syntax if one’s implementation is to multiplex between two independent backends.

Yes, assuming we can smooth out all the rough edges (which is the part I doubt), then this would be great for those users. However, what is the transition story for the users which want to do more. How would you explain to them that there is this other “expression command” (which also prints the result of the expression), which can support many other c++ features, but doesn’t do dynamic type resolution and whatever. And that we have “path expressions”, which guarantee that they won’t modify the state of the application, but don’t support something else.

And then there’s also the maintenance cost of supporting all of those implementations.

Well, I would say that even two commands are too much. As you’ve said, users just want to print stuff and get on with their life. I think that the fact that we have “expr” and “frame var” as separate commands is something born out of necessity and not a virtue.

I’d say that if we had a single print command that was sufficiently powerful for most users, then we could alias p to that, and there’d be no need for a DWIM cmd.

That does alleviate this part of my concern, though it has (possibly, depending on how it’s implemented) the unfortunate side-effect of making invalid expressions very expensive. If the user makes a mistake, then we will try to parse it both as a “frame var” expression (fast), and run it through the expression parser (not so fast).

I attempted to cover this in the “Expression Evaluation with Dynamic Types” section, specifically in “Dynamic Typing On-Demand”.

The quick summary is: DWIM printing doesn’t have to be one or the other – frame variable, or expression – it can be a collaboration of the two components.

The clang AST can be used to identify subexpressions that potentially require dynamic typing. In the example you gave, obj.x.get(), the AST would have obj.x as valid, and the .get() call as being invalid. Using the AST’s additional information, lldb can use its dynamic type knowledge on the valid data path, obj.x, to rewrite the expression so that evaluation can be reattempted. One way to do this is rewrite the expression is with a cast: static_cast<Derived&>(obj.x).get().

This exact situation would be a problem, which is why we’ve proposed the hybrid solution, so that user’s don’t hit weird inconsistencies such as the case you’ve described.

I understand what you’re trying to achieve, but I am not convinced that this can be done in the manner you’re proposing. For example, if I update my test case slightly to something like:

struct WithGetter { int v; int get() { return v; } };
struct Base { WithGetter x{47}; };
struct Derived : Base { WithGetter x{74}; };
Base &obj = ...;

what will get printed by obj.x.get() ?

Here, the expression is completely valid both for static and dynamic typing, but returns different results. Now sure, you can take even the valid expression ASTs, and try to apply dynamic typing on every step of the way, but that starts to become very similar to parsing the expression yourself (which why is suggested that). And somewhere along the way, I think this stops being a “command that, based on its input and context, determines which printing implementation to use”, and becomes an N+1th expression parser.

Thanks for such a detailed proposal, Dave, it gave me a lot to think about! First of all, I want to point out that the proposed approach of combining the clang parser and the “frame var” evaluator is a very interesting technical solution. I think it would work nicely in many situations and can improve the overall user experience.

However I’m not convinced that the suggested approach can cover all potential edge cases in a reliable and a non-surprising way for the user. @labath already showed a few examples where it’s not clear what the behaviour should be. I think having one single command that always works for all use cases is great, but I’m not sure this can be achieved with reasonable effort. Maybe instead we can get very far by improving the diagnostics and the discoverability of different features in a way that’s helpful to the user.

Given the way things work right now, I would imagine the print command do something like this:

def dwim_print(frame, expr):
    value = frame.GetValueForVariablePath(expr)
    if value.GetError().LooksLikeRealCppExpr():
        print("The input looks like a snippet of real C++, consider using `expr` command")
    else:
        print(value)

I would imagine that GetValueForVariablePath should support more things than it does now. Namely: arithmetic operations, type casts, simple function calls, dynamic type resolution and synthetic children providers (to some extent). This way the majority of use cases would be covered and most users would just use print <expr> and it would work. In rare cases the command would fail and suggest using the C++ expression compiler instead, which seems like a good balance between the user friendliness and implementation simplicity.

I think it might be helpful to daydream a bit and look beyond the current implementation (and its limitations) and think about what could be the “best possible” user experience :grinning:
Consider the following simplified example:

struct Foo { int x; };
struct Bar : Foo { float x; };
Bar* obj = new Bar{};

What should be the debugger behaviour if the user types print obj->x? The “frame var”, “expr” and DWIM will all give different results, which are all “correct” according to some specification. But what does the user want here? See the value of the x field. So maybe we should just show both of them?

(lldb) print obj->x
Found 2 fields in type `Bar`:
(Foo::x) (int) 24
(Bar::x) (float) 1.2

(lldb) print (double) obj->x
(double) 1.2
warning: type `Bar` has 2 matching fields. Picked `float Bar::x` because the static type of `obj` is `Bar`. Add an explicit cast to use the other field: (double) static_cast<Foo*>(obj)->x

Also you say:

This proposal assumes readers agree that expression evaluation can be fragile, slower, or both. The document focuses on the other issues …

I think if we’re looking at doing some major work around frame-var/expr/print, we should attempt to challenge these assumptions as well. The primary motivation for lldb-eval was better performance and the reliability of the LLDB’s expressions evaluator is a major concern for using LLDB as a primary debugger on scale. Expression evaluation is often used in data formatters (especially the pretty-printers inherited from GDB and used via GitHub - sivachandra/gala: An adapter package which enables GDB Python API in LLDB). I know it’s not recommended and everything can be done by manually manipulating the data, but a) it’s just so convenient and b) there’s already a bunch of legacy code. It’s also becoming more common to use LLDB in IDEs and text editors via DAP. It’s fairly common to have “watch” expressions there, which need to be fast and reliable.

It would print 47. The document proposes that dynamic typing happens “on demand”, ie only when needed to succeed in evaluation. Since this can succeed in evaluation without dynamic typing, no dynamic typing would happen and it would behave the same as expression.

Perhaps the best way to present this behavior is to compare it to an existing feature. When a user writes p a.b, and where a is a pointer type, then a fix-it is applied to correct the expression to a->b. The proposed behavior is akin to adding downcast fix-its.

Thank you!

While it would be great if there were no edge cases, the proposal is not explicitly aiming for zero edge cases. The nature of C++ might even make that impossible. Even if there were zero edge cases today, changes to C++ in the future could introduce some. And that’s considering only one language. The proposal aims to convenience the majority of users. If we have the opportunity improve the user experience for many users, with an implementation that has limited edge cases, that’s a tradeoff worth considering. What are the proportions needed to accept the tradeoff? I don’t know how we’d quantify it, but right now we have the one case that’s an issue (via @labath)

There’s also a very tricky (but not all too uncommon, particularly in some recursive template patterns), case where the subclass has a member which shadows a member of the base

  1. How common is this?
  2. Which recursive template patters use it?
  3. What do users do today to debug this?

To assess the tradeoff we need to answer these questions about the issue of shadowing. The exposure to this, and other edge cases, will vary by project. So far the discussion has been about C++, are the edge cases purely a C++ challenge?

Relatedly, we need to enumerate and identify any other cases where both dynamic and static typing are valid.

Conversely, in attempting to optimize for the very common cases, these questions guided our thinking:

  1. What percent of expression evaluation is a simple variable
  2. What percent of expressions use “basic” variable path (. or -> operators)
  3. What percent of expressions are a simple function/method expression?

We believe the vast majority of print expressions are these basic kinds, but don’t have numbers to share. We also see these as universally true, independent of the project.

Showing both is definitely a possibility, it’s a reasonable solution to the problem. It could be a bit slower, but if the user gives an expression that has multiple valid interpretations, then maybe that’s the price to pay. It would be more complicated to implement.

Another option is to pick one of the two (the dynamic one?) and print a message to indicate showing that another interpretation exists (the static one?), and how to evaluate it.

I briefly forgot that this has been discussed – but the challenge is that (it seems) lldb would have to parse every statement more than once, to see if there are multiple valid interpretations. As mentioned in another comment, we want to avoid clang parsing altogether where possible, and so having to parse most or all of the time seems not ideal.

I also believe it’s these are majority, maybe if you add type casts. Casting some address (or void*) to a pointer type is pretty common both in data formatters and ad-hoc commands.

That depends on the implementation, I guess? A custom expression parser/interpreter can easily detect multiple interpretations and return multiple values without re-parsing the expression. If we are to use clang parser for this, then yes, it sounds problematic. But the argument I’m trying to make is that an extended “frame var” interpreter (like lldb-eval) could be good enough for most cases.

And I completely agree with this strategy! :slight_smile: No need to use clang for simple expressions. Maybe I’m missing something, but this part is not very clear in your proposal. How would the expression be parsed in general case? Would you attempt GetValueForExpressionPath first and then go through clang parser with AST re-writing only if “frame var” evaluator failed?

Ok, but then you get this:

(lldb) dwim-p obj.x
(WithGetter) { v = 74 }
(lldb) dwim-p obj.x.get()
(int) 47

Yes, but fix-its have the very nice property of being “local”, which can be ensured because there’s just one parser. With two independent parsers, you can get this “spooky action at a distance”, where a modification of one part of the expression changes the meaning of a completely unrelated subexpression. (You could conjure up increasingly preposterous examples with a chain of dozens of member (.) subexpressions, where the addition of the last one changes the interpretation of the first member.)

That depends a lot on the code base, but a lot of the variadic templates boil down to some form of:

template<typename Head, typename... Tail>
Something<Head, Tail...> : public Something<Tail...> { Head head; };

STL is full of those (that’s how std::tuple, std::variant, etc. are implemented

I would assume they either print the full object (with or without a pretty printer), or they cast the object very carefully.

However, I don’t want to spend too much time discussion this. I’ll concede that most users will likely never encounter this corner case, but I still think it is a good way demonstrating that this is in fact a separate language. And I believe it would be better to replace an existing language than to add a new one.

After discussions with @labath, @JDevlieghere, @adrian.prantl, and others at the LLVM Dev Meeting, there was agreement that there will be a print command that tries other means to print, before falling back to expression. Some of the implementation details (of which there are multiple axes), are still in discussion. My understanding is that @werat will be publishing a related proposal soon.

In order to begin the work, I have opened a diff, ⚙ D138315 [lldb] Introduce dwim-print command, to lay the foundation of the dwim-print command. The initial implementation handles bare frame variables (in other words: not frame variable expressions) with frame variable, and otherwise falls back to expression evaluation.