More explanations on how the analyzer keeps making its way around the C++ AST.
== Lifetime extension ==
This is a brain dump of how (and how much) lifetime extension of temporary objects is currently broken in the static analyzert. Spoilers: not too much, it seems, but annoying nevertheless.
Consider an example:
1 class C \{
2 public:
3 C\(\) \{\}
4 \~C\(\) \{\}
5 \};
6
7 void foo\(\) \{
8 const C &c = C\(\);
9 \}
With the AST for the variable declaration:
DeclStmt 0x7fa5ac85cba0 <line:8:3, col:19>
\`\-VarDecl 0x7fa5ac85c878 <col:3, col:18> col:12 c 'const C &' cinit
\`\-ExprWithCleanups 0x7fa5ac85cb30 <col:16, col:18> 'const C' lvalue
\`\-MaterializeTemporaryExpr 0x7fa5ac85cb00 <col:16, col:18> 'const C' lvalue extended by Var 0x7fa5ac85c878 'c' 'const C &'
\`\-ImplicitCastExpr 0x7fa5ac85cae8 <col:16, col:18> 'const C' <NoOp>
\`\-CXXBindTemporaryExpr 0x7fa5ac85cac8 <col:16, col:18> 'C' \(CXXTemporary 0x7fa5ac85cac0\)
\`\-CXXTemporaryObjectExpr 0x7fa5ac85ca88 <col:16, col:18> 'C' 'void \(\)'
*here goes a periodic reminder that CXXTemporaryObjectExpr is a sub-class of CXXConstructExpr*
Notice how MaterializeTemporaryExpr is the innermost expression (the first one in the order of execution) that is an lvalue. Essentially, you can think of it as the mythical "rvalue-to-lvalue" cast that takes in a C++ object rvalue and returns the this-value for that object. Because all C++ objects have a this-value that never changes throughout their lifetime, it is essentially their identity. Otherwise you can't call methods on them.
MaterializeTemporaryExpr also contains information about the lifetime extension process: we needed the this-value in order to bind it to variable 'c'. You see that in the AST.
In the analyzer, however, MaterializeTemporaryExpr does a different thing, as a temporary solution (no pun intended). It constructs a new temporary region out of thin air and binds the rvalue object to that temporary in the Store. The respective function in our code is called "createTemporaryRegionIfNeeded". It also has a separate purpose of converting rvalue sub-object adjustments into lvalue sub-object adjustments, which we wouldn't discuss this time.
Now that we learned how to inline temporary constructors and destructors, it essentially means that the this-value in the constructor and in the destructor would be different. Because first we construct the object into temporary region R1, then we take lazyCompoundVal{R1} to represent the value of CXXTemporaryObjectExpr, then we materialize lazyCompoundVal{R1} to R2, then we bind R2 to variable 'c', then we call the automatic(!) destructor for 'c' which contains R2. To be clear, the region store at the time of destruction would be:
c: R2,
R2: lazyCompoundVal{R1}.
It means that fields of the object would contain the correct values, there would be the correct number of destructors called (no temporary destructors, just one automatic destructor), but the identity of the object (this-value) would change in the process. Unless the object actually makes any decisions or does any manipulations that involve its this-value, the modeling should be correct. When the object starts to actively use its this-value in its inlined methods, the analysis would do wrong stuff. Additionally, it causes a mess in the checkers when they try to track objects by their this-values - i.e. IteratorChecker has a lot of additional code to work around the fact that the region for the object constantly changes.
From the above it is clear that MaterializeTemporaryExpr should not construct any new regions, at least not in this case. We already have the correct region, namely R1, which should be re-used.
It is tempting to take R1 directly from lazyCompoundVal{R1} - it already has memories about once being a value of R1. I'm not sure it's not going to work - it may work, at least i'm not ready to come up with a counterexample. But the meaning of LazyCompoundVal's parent region is different and coincides with what we want just accidentally. Namely, lazyCompoundVal{R1} is a value of an object that was bound to R1 in some particular moment of time in the past, without any explanation of when this moment of time was - but there's no indication if R1 is the region of the temporary we've just constructed, or a region of an unrelated object that used to have the same value back then. As we'd see later, MaterializeTemporaryExpr doesn't always contain a constructor within it - there are a lot of cases to cover, and if the trick doesn't work even once, it's going to be hard, so i'd probably not going to commit into maintaining this invariant. Though it might be plausible to modify add an SVal kind that does exactly what we mean here - i.e. a value that does indeed correspond to a specific C++ object identified by region. It might be a beautiful solution, but it may also fail miserably if tricky cornercases show up - so i'm not ready to commit to that. Also the correct way of dealing with such values (i.e. for how long are they relevant?) would be extremely difficult to explain to checker developers.
The more straightforward approach here is to include MaterializeTemporaryExpr (hereinafter MTE) into the construction context. It means, if a temporary that we're about to construct would be lifetime-extended later, we'd rather know about that during construction, and maintain a map in the program state from MTE to their respective temporary regions that were used for representing the respective construction targets. Upon encountering the respective MTE we'd simply retrieve the implicit temporary storage for the value from the program state and declare that temporary region to be the value of the MTE. This would mimic the approach we have with CXXBindTemporaryExprs (hereinafter BTE) and their respective regions that allows temporary destructors to work - but this time it's going to be about MaterializeTemporaryExprs and automatic destructors. I imagine that on the checker side this can potentially be exposed via some sort of State->getTemporaryStorage(Expr) call, but i believe that generally this process should be as transparent to the checkers as possible.
It sounds as if both of these maps could be eliminated by always making sure that the target temporary is constructed "with" the MTE (for lifetime-extended temproraries) or BTE (for temporaries that require destruction at the end of full-expression). In this case, with the help of construction context-assisted lookahead, we declare that the target of the construction is CXXTempObjectRegion(MTE, LC) or CXXTempObjectRegion(BTE, LC) respectively, rather than CXXTempObjectRegion(CXXConstructExpr). Then during evaluation of MTE or BTE we'd simply construct the same region with the expression we're currently evaluating, and it's automagically going to be the correct region. This approach, however, becomes confusing when we start dealing with elidable constructors (explained below). So for now i believe that it is quite irrelevant which expression is identifying the temporary region.
== Elidable constructors ==
While it doesn't sound like an immediately important task to implement copy elision in the analyzer, it may help with making some things easier. And it'd also make some reports fancier, as mentioned in https://reviews.llvm.org/D43144.
Elidable copy-constructors can be explained as a form of lifetime extension. Instead of copying the temporary, they keep using the original value of the temporary, which in some pretty twisted sense means that they are lifetime-extending it to be able to use it. For example, if we modify our example by replacing the lifetime-extending reference variable with a value-type variable:
1 class C \{
2 public:
3 C\(\) \{\}
4 \~C\(\) \{\}
5 \};
6
7 void foo\(\) \{
8 C c = C\(\);
9 \}
...then we'd still have an MTE, even though lifetime extension would seem to be gone:
DeclStmt 0x7fb8f005afb8 <line:8:3, col:12>
\`\-VarDecl 0x7fb8f005ac50 <col:3, col:11> col:5 c 'C' cinit
\`\-ExprWithCleanups 0x7fb8f005afa0 <col:9, col:11> 'C'
\`\-CXXConstructExpr 0x7fb8f005af68 <col:9, col:11> 'C' 'void \(const C &\) noexcept' elidable
\`\-MaterializeTemporaryExpr 0x7fb8f005af00 <col:9, col:11> 'const C' lvalue
\`\-ImplicitCastExpr 0x7fb8f005aee8 <col:9, col:11> 'const C' <NoOp>
\`\-CXXBindTemporaryExpr 0x7fb8f005aec8 <col:9, col:11> 'C' \(CXXTemporary 0x7fb8f005aec0\)
\`\-CXXTemporaryObjectExpr 0x7fb8f005ae88 <col:9, col:11> 'C' 'void \(\)'
In this case the MTE is expressing the fact that the temporary constructed via CXXTemporaryObjectExpr can be "lifetime-extended" (by actually merging it with the stack variable) rather than copied, if the CXXConstructExpr surrounding it would be chosen to be elided. The AST doesn't make the elision choice for us - but is compatible with both choices. The MTE essentially overrides the necessity of immediate destruction provided by the BTE, and lets the surrounding AST decide upon the lifetime of the object.
Because the analyzer currently does not do copy elision, it will use the MTE only to compute the argument for the elidable copy-constructor, and then perform the copy-construction, and then destroy the original temporary at the end of the full-expression. Note, however, that in this case we need to properly support both the BTE (for the temporary destructor to work) and the MTE (for computing its value). We need to implement the MTE's ability to perform "rvalue-to-lvalue-cast" even if the temporary destruction is still necessary. For this reason, if we rely on constructing temporary regions with the correct BTEs or MTEs, at least one of these tasks becomes impossible to perform.
If we were to support copy elision, then the CXXTemporaryObjectExpr constructor would go directly into the variable region. For the purposes of modeling, it'd mean that only CXXTemporaryObjectExpr would actually need to be modeled. But this would require additional coding in the construction context to be able to realize that the target is the variable while modeling the CXXTemporaryObjectExpr.
For the sake of completeness, let's consider the ternary operator example:
1 class C \{
2 public:
3 C\(int\) \{\}
4 \~C\(\) \{\}
5 \};
6
7 void foo\(int coin\) \{
8 const C &c = coin ? C\(1\) : C\(2\);
9 \}
The respective AST would be:
DeclStmt 0x7fc1e20023e0 <line:8:3, col:34>
\`\-VarDecl 0x7fc1e2001dc8 <col:3, col:33> col:12 c 'const C &' cinit
\`\-ExprWithCleanups 0x7fc1e2002370 <col:16, col:33> 'const C' lvalue
\`\-MaterializeTemporaryExpr 0x7fc1e2002340 <col:16, col:33> 'const C' lvalue extended by Var 0x7fc1e2001dc8 'c' 'const C &'
\`\-ImplicitCastExpr 0x7fc1e2002328 <col:16, col:33> 'const C' <NoOp>
\`\-ConditionalOperator 0x7fc1e20022f8 <col:16, col:33> 'C'
>\-ImplicitCastExpr 0x7fc1e2002170 <col:16> 'bool' <IntegralToBoolean>
> \`\-ImplicitCastExpr 0x7fc1e2002158 <col:16> 'int' <LValueToRValue>
> \`\-DeclRefExpr 0x7fc1e2001e28 <col:16> 'int' lvalue ParmVar 0x7fc1e2001c18 'coin' 'int'
>\-CXXBindTemporaryExpr 0x7fc1e2002248 <col:23, col:26> 'C' \(CXXTemporary 0x7fc1e2002240\)
> \`\-CXXConstructExpr 0x7fc1e2002208 <col:23, col:26> 'C' 'void \(const C &\) noexcept' elidable
> \`\-MaterializeTemporaryExpr 0x7fc1e20021a0 <col:23, col:26> 'const C' lvalue
> \`\-ImplicitCastExpr 0x7fc1e2002188 <col:23, col:26> 'const C' <NoOp>
> \`\-CXXFunctionalCastExpr 0x7fc1e2002078 <col:23, col:26> 'C' functional cast to class C <ConstructorConversion>
> \`\-CXXBindTemporaryExpr 0x7fc1e2002058 <col:23, col:26> 'C' \(CXXTemporary 0x7fc1e2002050\)
> \`\-CXXConstructExpr 0x7fc1e2002018 <col:23, col:26> 'C' 'void \(int\)'
> \`\-IntegerLiteral 0x7fc1e2001e60 <col:25> 'int' 1
\`\-CXXBindTemporaryExpr 0x7fc1e20022d8 <col:30, col:33> 'C' \(CXXTemporary 0x7fc1e20022d0\)
\`\-CXXConstructExpr 0x7fc1e2002298 <col:30, col:33> 'C' 'void \(const C &\) noexcept' elidable
\`\-MaterializeTemporaryExpr 0x7fc1e2002280 <col:30, col:33> 'const C' lvalue
\`\-ImplicitCastExpr 0x7fc1e2002268 <col:30, col:33> 'const C' <NoOp>
\`\-CXXFunctionalCastExpr 0x7fc1e2002130 <col:30, col:33> 'C' functional cast to class C <ConstructorConversion>
\`\-CXXBindTemporaryExpr 0x7fc1e2002110 <col:30, col:33> 'C' \(CXXTemporary 0x7fc1e2002108\)
\`\-CXXConstructExpr 0x7fc1e20020d0 <col:30, col:33> 'C' 'void \(int\)'
\`\-IntegerLiteral 0x7fc1e20020b0 <col:32> 'int' 2
Each branch contains two constructors: the temporary and the elidable copy. The temporaries are surrounded with their respective BTEs and copy-elision-kind MTEs, which indicates that they need to be either destroyed as temporaries, or, if copy elision is chosen, have their lifetime decided upon by the surrounding AST. The elidable copy constructors also, being temporaries, have their respective BTEs. Note, however, that there is only one MTE for both BTEs for the elidable constructors.
So after the conditional operator is resolved (which is the first thing we need to do, according to the CFG), we'd go ahead and perform the constructors, and their trigger would be their respective BTE in the non-elide case, and the single top-level MTE in the elide case. In the non-elide case, copy constructors would be triggered by the top-level MTE.
It means that, once again, copy elision would prevent us from handling both the BTE and the copy-elision-kind MTE in the single construction, allowing the "predictable target region" trick to work: when we need the temporary destructor, we construct directly into CXXTempObjectRegion of the BTE and it gets automatically picked up during destruction, and when we need the automatic destructor, we construct directly into CXXTempObjectRegion of the MTE and we can easily compute the value of the MTE. But when we don't do copy elision, we'd have to keep at least one of those in the program state map.
== Return by value ==
Returning C++ objects by value is actually very similar to constructing it. Consider:
1 class C \{
2 public:
3 C\(\) \{\}
4 \~C\(\) \{\}
5 \};
6
7 C bar\(\) \{
8 C c;
9 return c;
10 \}
11
12 void foo\(\) \{
13 const C &c = bar\(\);
14 \}
With the respective AST for DeclStmt in foo():
DeclStmt 0x7fe62c84f8e8 <line:13:3, col:21>
\`\-VarDecl 0x7fe62c84f6b8 <col:3, col:20> col:12 c 'const C &' cinit
\`\-ExprWithCleanups 0x7fe62c84f878 <col:16, col:20> 'const C' lvalue
\`\-MaterializeTemporaryExpr 0x7fe62c84f848 <col:16, col:20> 'const C' lvalue extended by Var 0x7fe62c84f6b8 'c' 'const C &'
\`\-ImplicitCastExpr 0x7fe62c84f830 <col:16, col:20> 'const C' <NoOp>
\`\-CXXBindTemporaryExpr 0x7fe62c84f810 <col:16, col:20> 'C' \(CXXTemporary 0x7fe62c84f808\)
\`\-CallExpr 0x7fe62c84f7e0 <col:16, col:20> 'C'
\`\-ImplicitCastExpr 0x7fe62c84f7c8 <col:16> 'C \(\*\)\(\)' <FunctionToPointerDecay>
\`\-DeclRefExpr 0x7fe62c84f770 <col:16> 'C \(\)' lvalue Function 0x7fe62c84f190 'bar' 'C \(\)'
And for the ReturnStmt in bar():
ReturnStmt 0x7fe62c84f5b0 <line:9:3, col:10>
\`\-CXXConstructExpr 0x7fe62c84f578 <col:10> 'C' 'void \(const C &\) noexcept' elidable
\`\-ImplicitCastExpr 0x7fe62c84f518 <col:10> 'const C' lvalue <NoOp>
\`\-DeclRefExpr 0x7fe62c84f4f0 <col:10> 'C' lvalue Var 0x7fe62c84f280 'c' 'C'
Since https://reviews.llvm.org/D42875 we can already realize that the constructor in bar(), assuming that we're inlining bar() during analysis, would be constructed into something that is a return value of bar(). This allows us, by looking that the StackFrameContext's call site, to figure out that it is being constructed into the CallExpr in foo(). Now if only we knew that that the call site is a lifetime-extended temporary, i.e. if only we had a pointer to the foo()'s MTE at the CallExpr's CFG element, we'd be able to find the correct target region for construction: the CXXTempObjectRegion for the MTE in the StackFrameContext of foo(). So i'm proposing to add some sort of construction context to not only constructors, but also to functions that return objects, and then during construction perform the lookup in three easy steps:
1. in the callee's CFG from constructor to return statement,
2. through the location from the return statement to the call site,
3. then through the caller's CFG from the call site to the MTE.
If the function is not inlined, we can still make use of the construction context to represent the return value as a LazyCompoundValue of the MTE's temporary. It would eliminate the need to replace the return value with another value while evaluating the MTE, and of course the need to re-bind the object to a different this-region.
So i believe that this is a good way to eliminate the need for the "createTemporaryRegionIfNeeded" thing in the function calls as well.