The HLA compile-time language provides a powerful facility with which to write "programs" that execute while HLA is compiling your assembly language programs. Although it is possible to write some general-purpose programs using the HLA compile-time language, the real purpose of the HLA compile-time language is to allow you to write short programs that write other programs. In particular, the primary purpose of the HLA compile-time language is to automate the creation of large or complex assembly language sequences. The following subsections provide some simple examples of such compile-time programs.
Earlier, this book suggested that you could write programs to generate large, complex lookup tables for your assembly language programs (see the discussion of tables in 8.4.3 Generating Tables). Chapter 8 provides examples in HLA but suggests that writing a separate program is unnecessary. This is true; you can generate most lookup tables you'll need using nothing more than the HLA compile-time language facilities. Indeed, filling in table entries is one of the principle uses of the HLA compile-time language. In this section we will take a look at using the HLA compile-time language to construct data tables during compilation.
In 8.4.3 Generating Tables, you saw an example of an HLA program that writes a text file containing a lookup table for the trigonometric sine function. The table contains 360 entries with the index into the table specifying an angle in degrees. Each int32
entry in the table contains the value sin(angle )*1,000 where angle is equal to the index into the table. 8.4.3 Generating Tables suggests running this program and then including the text output from that program into the actual program that used the resulting table. You can avoid much of this work by using the compile-time language. The HLA program in Example 9-8 includes a short compile-time code fragment that constructs this table of sines directly.
Example 9-8. Generating a sine lookup table with the compile-time language
// demoSines.hla // // This program demonstrates how to create a lookup table // of sine values using the HLA compile-time language. program demoSines; #include( "stdlib.hhf" ) const pi :real80 := 3.1415926535897; readonly sines: int32[ 360 ] := [ // The following compile-time program generates // 359 entries (out of 360). For each entry // it computes the sine of the index into the // table and multiplies this result by 1000 // in order to get a reasonable integer value. ?angle := 0; #while( angle < 359 ) // Note: HLA's @sin function expects angles // in radians. radians = degrees*pi/180. // The int32 function truncates its result, // so this function adds 1/2 as a weak attempt // to round the value up. int32( @sin( angle * pi / 180.0 ) * 1000 + 0.5 ), ?angle := angle + 1; #endwhile // Here's the 360th entry in the table. This code // handles the last entry specially because a comma // does not follow this entry in the table. int32( @sin( 359 * pi / 180.0 ) * 1000 + 0.5 ) ]; begin demoSines; // Simple demo program that displays all the values in the table: for( mov( 0, ebx); ebx<360; inc( ebx )) do mov( sines[ ebx*4 ], eax ); stdout.put ( "sin( ", (type uns32 ebx ), " )*1000 = ", (type int32 eax ), nl ); endfor; end demoSines;
Another common use for the compile-time language is to build ASCII character lookup tables for use by the xlat
instruction at runtime. Common examples include lookup tables for alphabetic case manipulation. The program in Example 9-9 demonstrates how to construct an uppercase conversion table and a lowercase conversion table.[125] Note the use of a macro as a compile-time procedure to reduce the complexity of the table-generating code:
Example 9-9. Generating case-conversion tables with the compile-time language
// demoCase.hla // // This program demonstrates how to create a lookup table // of alphabetic case conversion values using the HLA // compile-time language. program demoCase; #include( "stdlib.hhf" ) const // emitCharRange // // This macro emits a set of character entries // for an array of characters. It emits a list // of values (with a comma suffix on each value) // from the starting value up to, but not including, // the ending value. #macro emitCharRange( start, last ): index; ?index:uns8 := start; #while( index < last ) char( index ), ?index := index + 1; #endwhile #endmacro; readonly // toUC: // The entries in this table contain the value of the index // into the table except for indices #$61..#$7A (those entries // whose indices are the ASCII codes for the lowercase // characters). Those particular table entries contain the // codes for the corresponding uppercase alphabetic characters. // If you use an ASCII character as an index into this table and // fetch the specified byte at that location, you will effectively // translate lowercase characters to uppercase characters and // leave all other characters unaffected. toUC: char[ 256 ] := [ // The following compile-time program generates // 255 entries (out of 256). For each entry // it computes toupper(index
) whereindex
is // the character whose ASCII code is an index // into the table. emitCharRange( 0, uns8('a') ) // Okay, we've generated all the entries up to // the start of the lowercase characters. Output // uppercase characters in place of the lowercase // characters here. emitCharRange( uns8('A'), uns8('Z') + 1 ) // Okay, emit the nonalphabetic characters // through to byte code #$FE: emitCharRange( uns8('z') + 1, $FF ) // Here's the last entry in the table. This code // handles the last entry specially because a comma // does not follow this entry in the table. #$FF ]; // The following table is very similar to the one above. // You would use this one, however, to translate uppercase // characters to lowercase while leaving everything else alone. // See the comments in the previous table for more details. TOlc: char[ 256 ] := [ emitCharRange( 0, uns8('A') ) emitCharRange( uns8('a'), uns8('z') + 1 ) emitCharRange( uns8('Z') + 1, $FF ) #$FF ]; begin demoCase; for( mov( uns32( ' ' ), eax ); eax <= $FF; inc( eax )) do mov( toUC[ eax ], bl ); mov( TOlc[ eax ], bh ); stdout.put ( "toupper( '", (type char al), "' ) = '", (type char bl), "' tolower( '", (type char al), "' ) = '", (type char bh), "'", nl ); endfor; end demoCase;
One important thing to note about this example is the fact that a semicolon does not follow the emitCharRange
macro invocations. Macro invocations do not require a closing semicolon. Often, it is legal to go ahead and add one to the end of the macro invocation because HLA is normally very forgiving about having extra semicolons inserted into the code. In this case, however, the extra semicolons are illegal because they would appear between adjacent entries in the TOlc
and toUC
tables. Keep in mind that macro invocations don't require a semicolon, especially when using macro invocations as compile-time procedures.
In the chapter on low-level control structures, this text points out that you can unravel loops to improve the performance of certain assembly language programs. One problem with unraveling, or unrolling, loops is that you may need to do a lot of extra typing, especially if there are many loop iterations. Fortunately, HLA's compile-time language facilities, especially the #while
and #for
loops, come to the rescue. With a small amount of extra typing plus one copy of the loop body, you can unroll a loop as many times as you please.
If you simply want to repeat the same exact code sequence some number of times, unrolling the code is especially trivial. All you have to do is wrap an HLA #for..#endfor
loop around the sequence and count off a val
object the specified number of times. For example, if you wanted to print Hello World
10 times, you could encode this as follows.
#for( count := 1 to 10 ) stdout.put( "Hello World", nl ); #endfor
Although the code above looks very similar to an HLA for
loop you could write in your program, remember the fundamental difference: The preceding code simply consists of 10 straight stdout.put
calls in the program. Were you to encode this using an HLA for
loop, there would be only one call to stdout.put
and lots of additional logic to loop back and execute that single call 10 times.
Unrolling loops becomes slightly more complicated if any instructions in that loop refer to the value of a loop control variable or another value, which changes with each iteration of the loop. A typical example is a loop that zeros the elements of an integer array:
mov( 0, eax ); for( mov( 0, ebx ); ebx < 20; inc( ebx )) do mov( eax, array[ ebx*4 ] ); endfor;
In this code fragment the loop uses the value of the loop control variable (in EBX) to index into array. Simply copying mov( eax, array[ ebx*4 ]);
20 times is not the proper way to unroll this loop. You must substitute an appropriate constant index in the range 0..76 (the corresponding loop indices, times 4) in place of ebx*4
in this example. Correctly unrolling this loop should produce the following code sequence:
mov( eax, array[ 0*4 ] ); mov( eax, array[ 1*4 ] ); mov( eax, array[ 2*4 ] ); mov( eax, array[ 3*4 ] ); mov( eax, array[ 4*4 ] ); mov( eax, array[ 5*4 ] ); mov( eax, array[ 6*4 ] ); mov( eax, array[ 7*4 ] ); mov( eax, array[ 8*4 ] ); mov( eax, array[ 9*4 ] ); mov( eax, array[ 10*4 ] ); mov( eax, array[ 11*4 ] ); mov( eax, array[ 12*4 ] ); mov( eax, array[ 13*4 ] ); mov( eax, array[ 14*4 ] ); mov( eax, array[ 15*4 ] ); mov( eax, array[ 16*4 ] ); mov( eax, array[ 17*4 ] ); mov( eax, array[ 18*4 ] ); mov( eax, array[ 19*4 ] );
You can easily do this using the following compile-time code sequence:
#for( iteration := 0 to 19 ) mov( eax, array[ iteration*4 ] ); #endfor
If the statements in a loop make use of the loop control variable's value, it is only possible to unroll such loops if those values are known at compile-time. You cannot unroll loops when user input (or other runtime information) controls the number of iterations.
[125] Note that on modern processors, using a lookup table is probably not the most efficient way to convert between alphabetic cases. However, this is just an example of filling in the table using the compile-time language. The principles are correct, even if the code is not exactly the best it could be.