8.3 Writing Compile-Time "Programs"
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.
8.3.1 Constructing Data Tables at Compile Time
Earlier, this text suggested that you could write programs to generate large, complex, lookup tables for your assembly language programs (see "Generating Tables" on page 651). That chapter provided examples in HLA but suggested that writing a separate program was unnecessary. This is true, you can generate most look-up 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 the section on generating tables, this text gave 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 contained the value sin(angle)*1000 where angle is equal to the index into the table. The section on generating tables suggested 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 following HLA program includes a short compile-time code fragment that constructs this table of sines directly.// 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; Program 8.10 Generating a SINE Lookup Table with the Compile-time Language
Another common use for the compile-time language is to build ASCII character lookup tables for use by the XLAT instruction at run-time. Common examples include lookup tables for alphabetic case manipulation. The following program demonstrates how to construct an upper case conversion table and a lower case conversion table1. Note the use of a macro as a compile-time procedure to reduce the complexity of the table generating code:// 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 pi :real80 := 3.1415926535897; // 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 indicies #$61..#$7A (those entries // whose indicies are the ASCII codes for the lower case // characters). Those particular table entries contain the // codes for the corresponding upper case 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 lower case characters to upper case 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 ) where index 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 lower case characters. Output // Upper Case characters in place of the lower // case characters here. emitCharRange( uns8('A'), uns8('Z') + 1 ) // Okay, emit the non-alphabetic 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 upper case // characters to lower case 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; Program 8.11 Generating Case Conversion Tables with the Compile-Time Language
One important thing to note about this sample 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.
8.3.2 Unrolling Loops
In the chapter on Low-Level Control Structures (see "Unraveling Loops" on page 800) this text points out that you can unravel loops to improve the performance of certain assembly language programs. One problem with unravelling, or unrolling, loops is that you may need to do a lot of extra typing, especially if many iterations are necessary. Fortunately, HLA's compile-time language facilities, especially the #WHILE loop, comes 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've got to do is wrap an HLA #WHILE..#ENDWHILE loop around the sequence and count down a VAL object the specified number of times. For example, if you wanted to print "Hello World" ten times, you could encode this as follows:?count := 0; #while( count < 10 ) stdout.put( "Hello World", nl ); ?count := count + 1; #endwhile
Although the code above looks very similar to a WHILE (or FOR) loop you could write in your program, remember the fundamental difference: the code above simply consists of ten straight stdout.put calls in the program. Were you to encode this using a FOR loop, there would be only one call to stdout.put and lots of additional logic to loop back and execute that single call ten times.
Unrolling loops becomes slightly more complicated if any instructions in that loop refer to the value of a loop control variable or other value that 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 ]);" twenty 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 four) 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 do this more efficiently using the following compile-time code sequence:?iteration := 0; #while( iteration < 20 ) mov( eax, array[ iteration*4 ] ); ?iteration := iteration+1; #endwhile
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 run-time information) controls the number of iterations.
8.4 Using Macros in Different Source Files
Unlike procedures, macros do not have a fixed piece of code at some address in memory. Therefore, you cannot create "external" macros and link them with other modules in your program. However, it is very easy to share macros with different source files - just put the macros you wish to reuse in a header file and include that file using the #include directive. You can make the macro will be available to any source file you choose using this simple trick.
8.5 Putting It All Together
This chapter has barely touched on the capabilities of the HLA macro processor and compile-time language. The HLA language has one of the most powerful macro processors around. None of the other 80x86 assemblers even come close to HLA's capabilities with regard to macros. Indeed, if you could say just one thing about HLA in relation to other assemblers, it would have to be that HLA's macro facilities are, by far, the best.
The combination of the HLA compile-time language and the macro processor give HLA users the ability to extend the HLA language in many ways. In the chapter on Domain Specific Languages, you'll get the opportunity to see how to create your own specialized languages using HLA's macro facilities.
Even if you don't do exotic things like creating your own languages, HLA's macro facilities and compile-time language are really great for automating code generation in your programs. The HLA Standard Library, for example, makes heavy use of HLA's macro facilities; "procedures" like stdout.put and stdin.get would be very difficult (if not impossible) to create without the power of HLA macro facilities and the compile-time language. For some good examples of the possible complexity one can achieve with HLA's macros, you should scan through the #include files in the HLA Standard Library and look at some of the macros appearing therein.
This chapter serves as a basic introduction to HLA's macro facilities. As you use macros in your own programs you will gain even more insight into their power. So by all means, use macros as much as you can - they can help reduce the effort needed to develop programs.
1Note 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.