On Mar 18, 2011, at 4:04 AM, Jim Goodnow II wrote:

> Hi Ted,
> 
> Okay, I agree and that was what I had originally intended, but ran into 
> difficulties implementing it.

Hi Jim,

I'm not surprised there were difficulties.  Some of the things that are needed 
in the analyzer core to implement the right design aren't there yet.  There are 
also some designed decisions that need to be discussed about how to do the 
correct interplay between the analyzer engine and checkers for the kind of 
tracking you want to do.

> So, as a first step, let's talk just about something simple:
> 
> it = v.begin();
> 
> Conceptually, the MemberCall for begin() returns a symbol region which I mark 
> as BeginValid. It is also linked with the MemRegion associated with the 
> instance or really any 'TypedRegion' associated with the Base of the 
> MemberCall which is the instance. When the symbol region is assigned to the 
> iterator, 'it', the BeginValid state gets propogated to the MemRegion 
> associated with the iterator.
> 
> First, does the returned symbol region actually get created by the engine or 
> do I have to do that in checkPostStmt(CXXMemberCall) or somewhere else?

The symbol region gets created by the analysis engine.  A checker should never 
create regions on its own.  Moreover, the region itself need not always be a 
symbolic region, but any TypedRegion.

Breaking it down from a design perspective, the analyzer engine handles the 
core semantics of expressions, and checkers reason about "higher-level" 
invariants, preconditions, postconditions, etc., that incorporate 
domain-specific knowledge.  I think this statement is fairly intuitive for 
"basic" expressions such as binary operators, but I think it's worth discussing 
what it means for function calls (which includes C++ method calls, operator 
overloading, and so forth).

Fundamentally, the analyzer engine should handle the "evaluation" of a function 
call.  This allows the analyzer engine to make decisions about analysis 
precision by sometimes inlining a function call, using an interprocedural 
summary, or taking a "conservative" approach when it has no knowledge of what a 
function does and treats the return value as being symbolic.  The last case is 
all we have implemented right now (although there is prototype support for 
inlining), but it is important to keep in mind that we have various potential 
options here.  To illustrate, consider the following code:

  int g() { return 42; }
  void test() { int x = g(); }

When analyzing 'test', the return value of the call to 'g' is a symbolic value. 
 That's because the analyzer engine doesn't look at the body of 'g' when 
evaluating the semantics of the function call 'g()'.  Instead, a symbolic value 
is created to represent some "abstract value" that is returned from 'g'.  This 
is a very conservative approach, but not very precise.  In scenarios like these 
we can possibly improve precision by inlining some function calls.  In this 
case, when analyzing 'test', we'd *know* that the value returned from the call 
to 'g' was 42.  Not a symbolic value, but 42.

This reasoning extends to functions returning pointers or references.  In this 
case, we use SymbolicRegions instead of just symbols, but they are basically 
the same thing (SymbolicRegions are based on symbols).  However, a called 
function that returns a pointer is not guaranteed to return a SymbolicRegion.  
For example:

  int *foo(int *x) { return x; }
  void test() {
     int a;
     int *b = foo(&a);
  }

In this example, the analyzer will currently evaluate the call to 'foo' as 
returning a SymbolicRegion instead of the VarRegion for 'a'.  That of course is 
imprecise; basic function inlining would show that 'foo' is the identity 
function, allowing us to analyze this code with perfect precision.

Calls to C++ functions/methods works the same way.  It doesn't matter how they 
are written (e.g., using operator overloading); at the end of the day they are 
still function calls, and we should leave the analyzer engine with the 
responsibility of handling their core semantics.

So the natural question is where does your checker fit into this picture?  
Checkers layer their own semantics by adding constraints and meta-state.  For 
example, let's say you had a checker that *knew* a priori that the function 'g' 
in my example above *always* returned the value 42.  It just knows this fact as 
part of its domain-specific knowledge without looking at the body of 'g'.  How 
should it enforce that knowledge?

My thought is that checkers can enforce such knowledge by using the 'assume' 
API, which adds constraints to values.  For example, in the checkPostStmt 
callback, the checker can use "GRState::assume(...)" to assume that the return 
value is equal to 42.  In the conservative case where the analyzer engine 
returns a symbolic value, the checker will be adding the constraint that the 
symbolic value is equal to the number 42.  In the more precise case where the 
analyzer engine inlines the function call, then the "assumption" will be that 
42 (the actual return value) is equal to 42 (the value assumed by the checker). 
 This is a tautology, so no constraints are added, but the logic is declarative 
and generic.

What happens if the checker assumes something that isn't compatible with the 
other constraints on the value (e.g., the value is 32 instead of 42)?  In that 
case, the call to "assume()" will return a null GRState*, and the state is 
infeasible.  That's either a bug in the checker (whose assumption was wrong) or 
a bug in the code, which the checker could report.  In any case, everything is 
logically consistent and composes naturally.  The checker doesn't need to 
specially handle SymbolicRegions, since everything is declarative.

Now let's talk about specifics for your checker.

> Conceptually, the MemberCall for begin() returns a symbol region which I mark 
> as BeginValid.

Instead of reasoning about a symbolic region, the checker only cares that the 
call to begin() just returns a region.  Instead of reasoning about symbols, 
that region can be marked as "BeginValid."

> It is also linked with the MemRegion associated with the instance or really 
> any 'TypedRegion' associated with the Base of the MemberCall which is the 
> instance.

This is another example of a relationship that the checker reasons about, and 
not the analyzer engine.  It makes sense to track this relationship using an 
ImmutableMap in the GDM, as it represents an "invariant" or "relationship" 
between those two regions that goes beyond reasoning about raw expression 
semantics.

> When the symbol region is assigned to the iterator, 'it', the BeginValid 
> state gets propogated to the MemRegion associated with the iterator.


This is the tricky part.  The low-level semantics of C++ actually involve 
either an assignment or a copy constructor.  Technically a new object is 
getting created/initialized, which means we need to think about yet another 
region.  The analyzer engine should handle that part.  It should reason about 
the copy constructor effects (if any), just like any other function call.

What the analyzer engine *does not* handle is that the relationships you were 
tracking for the iterator region get propagated to the other iterator.  That 
needs to be handled by the checker.  This could be done either as a 
"postcondition", e.g., in checkPostStmt(), or we can consider adding additional 
API hooks to checker to have the analyzer help checkers propagate such 
relationships.

Fundamentally, the propagation of such relationships is the checker's 
responsibility because the checker is "declaring" the postconditions of the 
assignment.  The postcondition is that the assigned iterator has the same 
iterator semantics as the original.  What that mean's is up to the checker, but 
it's the checker's responsibility to declare and enforce those constraints.  We 
can add additional APIs to Checker that the analyzer engine can call to help 
make this more automated, but fundamentally that's the design we're going for.

> 
> Second, do I still need to handle the assignment in OperatorCall since it is 
> essentially a structure/class assignment or will that be handled by the 
> Engine logic?

I think I just answered this question.  Fundamentally, the analyzer engine 
should handle the function call and the structure/class assignment, but the 
Checker will need to interplay with this logic by causing the iterator 
"invariants" and "constraints" to propagate forward after the copy.

Does this clarify things a bit?  I know this is a bit high-level, and there are 
certainly pieces of the analyzer engine that are currently missing that don't 
implement all the pieces I described, but I think this is the right design we 
should go for.  It clearly separates responsibilities of who handles which 
semantics, and tries to get checkers out of the business of evaluating the raw 
semantics of expressions.

Cheers,
Ted
_______________________________________________
cfe-commits mailing list
[email protected]
http://lists.cs.uiuc.edu/mailman/listinfo/cfe-commits

Reply via email to