4. Control Flow Analysis
- 4.1. Unreachable code analysis
- 4.2. Case fall through
- 4.3. Enumerations controlling switch statements
- 4.4. Empty if statements
- 4.5. Use of assignments as control expressions
- 4.6. Constant control expressions
- 4.7. Conditional and iteration statements
- 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 }