7.7 Conditional Compilation (Compile-Time Decisions)
HLA's compile-time language provides an IF statement, #IF, that lets you make various decisions at compile-time. The #IF statement has two main purposes: the traditional use of #IF is to support conditional compilation (or conditional assembly) allowing you to include or exclude code during a compilation depending on the status of various symbols or constant values in your program. The second use of this statement is to support the standard IF statement decision making process in the HLA compile-time language. This section will discuss these two uses for the HLA #IF statement.
The simplest form of the HLA compile-time #IF statement uses the following syntax:#if( constant_boolean_expression ) << text >> #endif
Note that you do not place semicolons after the #ENDIF clause. If you place a semicolon after the #ENDIF, it becomes part of the source code and this would be identical to inserting that semicolon immediately before the next text item in the program.
At compile-time, HLA evaluates the expression in the parentheses after the #IF. This must be a constant expression and its type must be boolean. If the expression evaluates true, then HLA continues processing the text in the source file as though the #IF statement were not present. However, if the expression evaluates false, then HLA treats all the text between the #IF and the corresponding #ENDIF clause as though it were a comment (i.e., it ignores this text).
Figure 7.2 Operation of HLA Compile-Time #IF Statement
Keep in mind that HLA's constant expressions support a full expression syntax like you'd find in a high level language like C or Pascal. The #IF expression syntax is not limited as are expressions in the HLA IF statement. Therefore, it is perfectly reasonable to write fancy expressions like the following:#if( @length( someStrConst ) < 10 & ( MaxItems*2 < 100 | MinItems-5 < 10 )) << text >> #endif
Keep in mind that the items in a compile-time expression must all be CONST or VAL identifiers or an HLA compile-time function call (with appropriate parameters). In particular, remember that HLA evaluates these expressions at compile-time so they cannot contain run-time variables1. Also note that HLA's compile time language uses complete boolean evaluation, so any side effects that occur in the expression may produce undesired results.
The HLA #IF statement supports optional #ELSEIF and #ELSE clauses that behave in the intuitive fashion. The complete syntax for the #IF statement looks like the following:#if( constant_boolean_expression1 ) << text1 >> #elseif( constant_boolean_expression2 ) << text2 >> #else << text3 >> #endif
If the first boolean expression evaluates true then HLA processes the text up to the #ELSEIF clause. It then skips all text (i.e., treats it like a comment) until it encounters the #ENDIF clause. HLA continues processing the text after the #ENDIF clause in the normal fashion.
If the first boolean expression above evaluates false, then HLA skips all the text until it encounters a #ELSEIF, #ELSE, or #ENDIF clause. If it encounters a #ELSEIF clause (as above), then HLA evaluates the boolean expression associated with that clause. If it evaluates true, then HLA processes the text between the #ELSEIF and the #ELSE clauses (or to the #ENDIF clause if the #ELSE clause is not present). If, during the processing of this text, HLA encounters another #ELSEIF or, as above, a #ELSE clause, then HLA ignores all further text until it finds the corresponding #ENDIF.
If both the first and second boolean expressions in the example above evaluate false, then HLA skips their associated text and begins processing the text in the #ELSE clause. As you can see, the #IF statement behaves in a relatively intuitive fashion once you understand how HLA "executes" the body of these statements (that is, it processes the text or treats it as a comment depending on the state of the boolean expression). Of course, you can create a nearly infinite variety of different #IF statement sequences by including zero or more #ELSEIF clauses and optionally supplying the #ELSE clause. Since the construction is identical to the HLA IF..THEN..ELSEIF..ELSE..ENDIF statement, there is no need to elaborate further here.
A very traditional use of conditional compilation is to develop software that you can easily configure for several different environments. For example, the FCOMIP instruction makes floating point comparisons very easy but this instruction is available only on Pentium Pro and later processors. If you want to use this instruction on the processors that support it, and fall back to the standard floating point comparison on the older processors you would normally have to write two versions of the program - one with the FCOMIP instruction and one with the traditional floating point comparison sequence. Unfortunately, maintaining two different source files (one for newer processors and one for older processors) is very difficult. Most engineers prefer to use conditional compilation to embed the separate sequences in the same source file. The following example demonstrates how to do this.const PentProOrLater: boolean := false; // Set true to use FCOMIxx instrs. . . . #if( PentProOrLater ) fcomip(); // Compare st1 to st0 and set flags. #else fcomp(); // Compare st1 to st0. fstsw( ax ); // Move the FPU condition code bits sahf(); // into the FLAGS register. #endif
As currently written, this code fragment will compile the three instruction sequence in the #ELSE clause and ignore the code between the #IF and #ELSE clauses (because the constant PentProOrLater is false). By changing the value of PentProOrLater to true, you can tell HLA to compile the single FCOMIP instruction rather than the three-instruction sequence. Of course, you can use the PentProOrLater constant in other #IF statements throughout your program to control how HLA compiles your code.
Note that conditional compilation does not let you create a single executable that runs efficiently on all processors. When using this technique you will still have to create two executable programs (one for Pentium Pro and later processors, one for the earlier processors) by compiling your source file twice; during the first compilation you must set the PentProOrLater constant to false, during the second compilation you must set this constant to true. Although you must create two separate executables, you need only maintain a single source file.
If you are familiar with conditional compilation in other languages, such as the C/C++ language, you may be wondering if HLA supports a statement like C's "#ifdef" statement. The answer is no, it does not. However, you can use the HLA compile-time function @DEFINED to easily test to see if a symbol has been defined earlier in the source file. Consider the following modification to the above code that uses this technique:const // Note: uncomment the following line if you are compiling this // code for a Pentium Pro or later CPU. // PentProOrLater :=0; // Value and type are irrelevant . . . #if( @defined( PentProOrLater ) ) fcomip(); // Compare st1 to st0 and set flags. #else fcomp(); // Compare st1 to st0. fstsw( ax ); // Move the FPU condition code bits sahf(); // into the FLAGS register. #endif
Another common use of conditional compilation is to introduce debugging and testing code into your programs. A typical debugging technique that many HLA programmers use is to insert "print" statements at strategic points throughout their code in order to trace through their code and display important values at various checkpoints. A big problem with this technique is that they must remove the debugging code prior to completing the project. The software's customer (or a student's instructor) probably doesn't want to see debugging output in the middle of a report the program produces. Therefore, programmers who use this technique tend to insert code temporarily and then remove the code once they run the program and determine what is wrong. There are at least two problems with this technique:
- Programmers often forget to remove some debugging statements and this creates defects in the final program, and
- After removing a debugging statement, these programmers often discover that they need that same statement to debug some different problem at a later time. Hence they are constantly inserting, removing, and inserting the same statements over and over again.
Conditional compilation can provide a solution to this problem. By defining a symbol (say, debug) to control debug output in your program, you can easily activate or deactivate all debugging output by simply modifying a single line of source code. The following code fragment demonstrates this:const debug: boolean := false; // Set to true to activate debug output. . . . #if( debug ) stdout.put( "At line ", @lineNumber, " i=", i, nl ); #endif
As long as you surround all debugging output statements with a #IF statement like the one above, you don't have to worry about debug output accidentally appearing in your final application. By setting the debug symbol to false you can automatically disable all such output. Likewise, you don't have to remove all your debugging statements from your programs once they've served their immediate purpose. By using conditional compilation, you can leave these statements in your code because they are so easy to deactivate. Later, if you decide you need to view this same debugging information during a program run, you won't have to reenter the debugging statement - you simply reactivate it by setting the debug symbol to true.
We will return to this issue of inserting debugging code into your programs in the chapter on macros.
Although program configuration and debugging control are two of the more common, traditional, uses for conditional compilation, don't forget that the #IF statement provides the basic conditional statement in the HLA compile-time language. You will use the #IF statement in your compile-time programs the same way you would use an IF statement in HLA or some other language. Later sections in this text will present lots of examples of using the #IF statement in this capacity.
7.8 Repetitive Compilation (Compile-Time Loops)
HLA's #WHILE..#ENDWHILE statement provides a compile-time loop construct. The #WHILE statement tells HLA to repetitively process the same sequence of statements during compilation. This is very handy for constructing data tables (see "Constructing Data Tables at Compile Time" on page 996) as well as providing a traditional looping structure for compile-time programs. Although you will not employ the #WHILE statement anywhere near as often as the #IF statement, this compile-time control structure is very important when writing advanced HLA programs.
The #WHILE statement uses the following syntax:#while( constant_boolean_expression ) << text >> #endwhile
When HLA encounters the #WHILE statement during compilation, it will evaluate the constant boolean expression. If the expression evaluates false, then HLA will skip over the text between the #WHILE and the #ENDWHILE clause (the behavior is similar to the #IF statement if the expression evaluates false). If the expression evaluates true, then HLA will process the statements between the #WHILE and #ENDWHILE clauses and then "jump back" to the start of the #WHILE statement in the source file and repeat this process.
Figure 7.3 HLA Compile-Time #WHILE Statement Operation
To understand how this process works, consider the following program:program ctWhile; #include( "stdlib.hhf" ) static ary: uns32 := [ 2, 3, 5, 8, 13 ]; begin ctWhile; ?i := 0; #while( i < 5 ) stdout.put( "array[ ", i, " ] = ", ary[i*4], nl ); ?i := i + 1; #endwhile end ctWhile; Program 7.2 #WHILE..#ENDWHILE Demonstration
As you can probably surmise, the output from this program is the following:array[ 0 ] = 2 array[ 1 ] = 3 array[ 2 ] = 4 array[ 3 ] = 5 array[ 4 ] = 13
What is not quite obvious is how this program generates this output. Remember, the #WHILE..#ENDWHILE construct is a compile-time language feature, not a run-time control construct. Therefore, the #WHILE loop above repeats five times during compilation. On each repetition of the loop, the HLA compiler processes the statements between the #WHILE and #ENDWHILE clauses. Therefore, the program above is really equivalent to the following:program ctWhile; #include( "stdlib.hhf" ) static ary: uns32 := [ 2, 3, 5, 8, 13 ]; begin ctWhile; stdout.put( "array[ ", 0, " ] = ", ary[0*4], nl ); stdout.put( "array[ ", 1, " ] = ", ary[1*4], nl ); stdout.put( "array[ ", 2, " ] = ", ary[2*4], nl ); stdout.put( "array[ ", 3, " ] = ", ary[3*4], nl ); stdout.put( "array[ ", 4, " ] = ", ary[4*4], nl ); end ctWhile; Program 7.3 Program Equivalent to the Code in Program 7.2
As you can see, the #WHILE statement is very convenient for constructing repetitive code sequences. This is especially invaluable for unrolling loops. Additional uses of the #WHILE loop appear in later sections of this text.
7.9 Putting It All Together
The HLA compile-time language provides considerable power. With the compile-time language you can automate the generation of tables, selectively compile code for different environments, easily unroll loops to improve performance, and check the validity of code you're writing. Combined with macros and other features that HLA provides, the compile-time language is probably the premier feature of the HLA language - no other assembler provides comparable features. For more information about the HLA compile time language, be sure to read the next chapter on macros.
1Except, of course, as parameters to certain HLA compile-time functions like @size or @typeName.