4. Control Flow Analysis

  1. 4.1. Unreachable code analysis
  2. 4.2. Case fall through
  3. 4.3. Enumerations controlling switch statements
  4. 4.4. Empty if statements
  5. 4.5. Use of assignments as control expressions
  6. 4.6. Constant control expressions
  7. 4.7. Conditional and iteration statements
  8. 4.8. Exception analysis

The checker has a number of features which can be used to help track down potential programming errors relating to the use of variables within a source file and the flow of control through the program. Examples of this are detecting sections of unused code, and flagging expressions that depend upon the order of evaluation where the order is not defined.

4.1. Unreachable code analysis

Consider the following function definition:

int f ( int n )
{
	if ( n ) {
		return ( 1 );
	} else {
		return ( 0 );
	}
	return ( 2 );
}

The final return statement is redundant since it can never be reached. The test for unreachable code is controlled by:

#pragma TenDRA unreachable code permit

where permit is replaced by disallow to give an error if unreached code is detected, warning to give a warning, or allow to disable the test (this is the default).

There are also equivalent command-line options to tcc of the form -X:unreached=state, where state can be check, warn or dont.

Annotations to the code in the form of user-defined keywords may be used to indicate that a certain statement is genuinely reached or unreached. These keywords are introduced using:

#pragma TenDRA keyword REACHED for set reachable
#pragma TenDRA keyword UNREACHED for set unreachable

The statement REACHED then indicates that this portion of the program is actually reachable, whereas UNREACHED indicates that it is unreachable. For example, one way of fixing the program above might be to say that the final return is reachable (this is a blatant lie, but never mind). This would be done as follows:

int f ( int n ) {
	if ( n ) {
		return ( 1 );
	} else {
		return ( 0 );
	}
	REACHED
	return ( 2 );
}

An example of the use of UNREACHED might be in the function below which falls out of the bottom without a return statement. We might know that, because it is never called with c equal to zero, the end of the function is never reached. This could be indicated as follows:

int f ( int c ) {
	if ( c ) return ( 1 );
	UNREACHED
}

As always, if new keywords are introduced into a program then definitions need to be provided for conventional compilers. In this case, this can be done as follows:

#ifdef __TenDRA__
#pragma TenDRA keyword REACHED for set reachable
#pragma TenDRA keyword UNREACHED for set unreachable
#else
#define REACHED
#define UNREACHED
#endif

The directive:

#pragma TenDRA unreachable code allow

enables a flow analysis check to detect unreachable code. It is possible to assert that a statement is reached or not reached by preceding it by a keyword introduced by one of the directives:

#pragma TenDRA keyword identifier for set reachable
#pragma TenDRA keyword identifier for set unreachable

The fact that certain functions, such as exit, do not return a value can be exploited in the flow analysis routines. The equivalent directives:

#pragma TenDRA bottom identifier
#pragma TenDRA++ type identifier for bottom

can be used to introduce a typedef declaration for the type, bottom, returned by such functions. The TenDRA API headers declare exit and similar functions in this way, for example:

#pragma TenDRA bottom __bottom
__bottom exit ( int ) ;
__bottom abort ( void ) ;

The bottom type is compatible with void in function declarations to allow such functions to be redeclared in their conventional form.

4.2. Case fall through

Another flow analysis check concerns fall through in case statements. For example, in:

void f ( int n )
{
	switch ( n ) {
		case 1 : puts ( "one" );
		case 2 : puts ( "two" );
	}
}

the control falls through from the first case to the second. This may be due to an error in the program (a missing break statement), or be deliberate. Even in the latter case, the code is not particularly maintainable as it stands - there is always the risk when adding a new case that it will interrupt this carefully contrived flow. Thus it is customary to comment all case fall throughs to serve as a warning.

In the default mode, the TenDRA C checker ignores all such fall throughs. A check to detect fall through in case statements is controlled by:

#pragma TenDRA fall into case permit

where permit is allow (no errors), warning (warn about case fall through) or disallow (raise errors for case fall through).

There are also equivalent command-line options to tcc of the form -X:fall_thru=state, where state can be check, warn or dont.

Deliberate case fall throughs can be indicated by means of a keyword, which has been introduced using:

#pragma TenDRA keyword FALL_THROUGH for fall into case

Then, if the example above were deliberate, this could be indicated by:

void f ( int n ) {
	switch ( n ) {
		case 1 : puts ( "one" );
		FALL_THROUGH
		case 2 : puts ( "two" );
	}
}

Note that FALL_THROUGH is inserted between the two cases, rather than at the end of the list of statements following the first case.

If a keyword is introduced in this way, then an alternative definition needs to be introduced for conventional compilers. This might be done as follows:

#ifdef __TenDRA__
#pragma TenDRA keyword FALL_THROUGH for fall into case
#else
#define FALL_THROUGH
#endif

4.3. Enumerations controlling switch statements

Enumerations are commonly used as control expressions in switch statements. When case labels for some of the enumeration constant belonging to the enumeration type do not exist and there is no default label, the switch statement has no effect for certain possible values of the control expression. Checks to detect such switch statements are controlled by:

#pragma TenDRA enum switch analysis status

where status is on (raise an error), warning (produce a warning), or off (the default mode when no errors are produced).

4.4. Empty if statements

Consider the following C statements:

if ( var1 == 1 ) ;
	var2 = 0 ;

The conditional statement serves no purpose here and the second statement will always be executed regardless of the value of var1. This is almost certainly not what the programmer intended to write. A test for if statements with no body is controlled by:

#pragma TenDRA extra ; after conditional permit

with the usual allow (this is the default setting), warning and disallow options for permit.

4.5. Use of assignments as control expressions

Using the C assignment operator, =, when the equality operator == was intended is an extremely common problem. The pragma:

#pragma TenDRA assignment as bool permit

is used to control the treatment of assignments used as the controlling expression of a conditional statement or a loop, e.g.

if( var = 1 ) { ...

The options for permit are allow, warning and disallow. The default setting allows assignments to be used as control statements without raising an error.

4.6. Constant control expressions

Statements with constant control expressions are not really conditional at all since the value of the control statement can be evaluated statically. Although this feature is sometimes used in loops, relying on a break, goto or return statement to end the loop, it may be useful to detect all constant control expressions to check that they are deliberate. The check for statically constant control expressions is controlled using:

#pragma TenDRA const conditional permit

where permit may be replaced by disallow to give an error when constant control expressions are encountered, warning to replace the error by a warning, or the check may be switched off using allow (this is the default).

4.7. Conditional and iteration statements

The directive:

#pragma TenDRA const conditional allow

can be used to enable a check for constant expressions used in conditional contexts. A literal constant is allowed in the condition of a while , for or do statement to allow for such common constructs as:

while ( true ) {
	// while statement body
}

and target dependent constant expressions are allowed in the condition of an if statement, but otherwise constant conditions are reported according to the status of this check.

The common error of writing = rather than == in conditions can be detected using the directive:

#pragma TenDRA assignment as bool allow

which can be used to disallow such assignment expressions in contexts where a boolean is expected. The error message can be suppressed by enclosing the assignment within parentheses.

Another common error associated with iteration statements, particularly with certain brace styles, is the accidental insertion of an extra semicolon as in:

for ( init ; cond ; step ) ;
{
	// for statement body
}

The directive:

#pragma TenDRA extra ; after conditional allow

can be used to enable a check for such suspicious empty iteration statement bodies (it actually checks for ;{).

4.8. Exception analysis

The ISO C++ rules do not require exception specifications to be checked statically. This is to facilitate the integration of large systems where a single change in an exception specification could have ramifications throughout the system. However it is often useful to apply such checks, which can be enabled using the directive:

#pragma TenDRA++ throw analysis on

This detects any potentially uncaught exceptions and other exception problems. In the error messages arising from this check, an uncaught exception of type ... means that an uncaught exception of an unknown type (arising, for example, from a function without an exception specification) may be thrown. For example:

void f ( int ) throw ( int ) ;
void g ( int ) throw ( long ) ;
void h ( int ) ;

void e () throw ( int )
{
	f ( 1 ) ;   // OK
	g ( 2 ) ;   // uncaught 'long' exception
	h ( 3 ) ;   // uncaught '...' exception
}