9.8 Macros (Compile-Time Procedures)

Macros are objects that a language processor replaces with other text during compilation. Macros are great devices for replacing long, repetitive sequences of text with much shorter sequences of text. In additional to the traditional role that macros play (e.g., #define in C/C++), HLA's macros also serve as the equivalent of a compile-time language procedure or function. Therefore, macros are very important in HLA's compile-time language—just as important as functions and procedures are in other high-level languages.

Although macros are nothing new, HLA's implementation of macros far exceeds the macro-processing capabilities of most other programming languages (high level or low level). The following sections explore HLA's macro-processing facilities and the relationship between macros and other HLA CTL control constructs.

HLA supports a straightforward macro facility that lets you define macros in a manner that is similar to declaring a procedure. A typical, simple macro declaration takes the following form:

#macro macroname;
     << Macro body >>
#endmacro

Although macro and procedure declarations are similar, there are several immediate differences between the two that are obvious from this example. First, of course, macro declarations use the reserved word #macro rather than procedure. Second, you do not begin the body of the macro with a begin macroname; clause. Finally, you will note that macros end with the #endmacro clause rather than end macroname;. The following code is a concrete example of a macro declaration:

#macro neg64;

     neg( edx );
     neg( eax );
     sbb( 0, edx );

#endmacro

Execution of this macro's code will compute the two's complement of the 64-bit value in EDX:EAX (see the description of extended-precision neg in 8.1.7 Extended-Precision neg Operations).

To execute the code associated with neg64, you simply specify the macro's name at the point you want to execute these instructions. For example:

mov( (type dword i64), eax );
     mov( (type dword i64[4]), edx );
     neg64;

Note that you do not follow the macro's name with a pair of empty parentheses as you would a procedure call (the reason for this will become clear a little later).

Other than the lack of parentheses following neg64's invocation,[122] this looks just like a procedure call. You could implement this simple macro as a procedure using the following procedure declaration:

procedure neg64p;
begin neg64p;

     neg( edx );
     neg( eax );
     sbb( 0, edx );

end neg64p;

Note that the following two statements will both negate the value in EDX:EAX:

neg64;          neg64p();

The difference between these two (the macro invocation versus the procedure call) is the fact that macros expand their text inline, whereas a procedure call emits a call to the corresponding procedure elsewhere in the text. That is, HLA replaces the invocation neg64; directly with the following text:

neg( edx );
     neg( eax );
     sbb( 0, edx );

On the other hand, HLA replaces the procedure call neg64p(); with the single call instruction:

call neg64p;

Presumably, you've defined the neg64p procedure earlier in the program.

You should make the choice of macro versus procedure call on the basis of efficiency. Macros are slightly faster than procedure calls because you don't execute the call and corresponding ret instructions. On the other hand, the use of macros can make your program larger because a macro invocation expands to the text of the macro's body on each invocation. Procedure calls jump to a single instance of the procedure's body. Therefore, if the macro body is large and you invoke the macro several times throughout your program, it will make your final executable much larger. Also, if the body of your macro executes more than a few simple instructions, the overhead of a call/ret sequence has little impact on the overall execution time of the code, so the execution time savings are nearly negligible. On the other hand, if the body of a procedure is very short (like the neg64 example above), you'll discover that the macro implementation is much faster and doesn't expand the size of your program by much. A good rule of thumb is:

Note

Use macros for short, time-critical program units. Use procedures for longer blocks of code and when execution time is not as critical.

Macros have many other disadvantages over procedures. Macros cannot have local (automatic) variables, macro parameters work differently than procedure parameters, macros don't support (runtime) recursion, and macros are a little more difficult to debug than procedures (just to name a few disadvantages). Therefore, you shouldn't really use macros as a substitute for procedures except in cases where performance is absolutely critical.

Like procedures, macros allow you to define parameters that let you supply different data on each macro invocation. This lets you write generic macros whose behavior can vary depending on the parameters you supply. By processing these macro parameters at compile time, you can write very sophisticated macros.

Macro parameter declaration syntax is very straightforward. You simply supply a list of parameter names within parentheses in a macro declaration:

#macro neg64( reg32HO, reg32LO );

     neg( reg32HO );
     neg( reg32LO );
     sbb( 0, reg32HO );

#endmacro;

Note that you do not associate a data type with a macro parameter as you do for procedural parameters. This is because HLA macros are generally text objects.

When you invoke a macro, you simply supply the actual parameters the same way you would for a procedure call:

neg64( edx, eax );

Note that a macro invocation that requires parameters expects you to enclose the parameter list within parentheses.

As the previous section explains, HLA automatically associates the type text with macro parameters. This means that during a macro expansion, HLA substitutes the text you supply as the actual parameter everywhere the formal parameter name appears. The semantics of "pass by textual substitution" are a little different than "pass by value" or "pass by reference," so it is worthwhile exploring those differences here.

Consider the following macro invocations, using the neg64 macro from the previous section:

neg64( edx, eax );
     neg64( ebx, ecx );

These two invocations expand into the following code:

// neg64(edx, eax );

     neg( edx );
     neg( eax );
     sbb( 0, edx );

// neg64( ebx, ecx );

     neg( ebx );
     neg( ecx );
     sbb( 0, ebx );

Note that macro invocations do not make a local copy of the parameters (as "pass by value" does), nor do they pass the address of the actual parameter to the macro. Instead, a macro invocation of the form neg64( edx, eax ); is equivalent to the following:

?reg32HO: text := "edx";
  ?reg32LO: text := "eax";

  neg( reg32HO );
  neg( reg32LO );
  sbb( 0, reg32HO );

Of course, the text objects immediately expand their string values inline, producing the former expansion for neg64( edx, eax );.

Note that macro parameters are not limited to memory, register, or constant operands as are instruction or procedure operands. Any text is fine as long as its expansion is legal wherever you use the formal parameter. Similarly, formal parameters may appear anywhere in the macro body, not just where memory, register, or constant operands are legal. Consider the following macro declaration and sample invocations:

#macro chkError( instr, jump, target );

     instr;
     jump target;

#endmacro;

     chkError( cmp( eax, 0 ), jnl, RangeError );       // Example 1
          ...
     chkError( test( 1, bl ), jnz, ParityError );      // Example 2

// Example 1 expands to

     cmp( eax, 0 );
     jnl RangeError;

// Example 2 expands to

     test( 1, bl );
     jnz ParityError;

In general, HLA assumes that all text between commas constitutes a single macro parameter. If HLA encounters any opening bracketing symbols (left parentheses, left braces, or left brackets), then it will include all text up to the appropriate closing symbol, ignoring any commas that may appear within the bracketing symbols. This is why the chkError invocations above treat cmp( eax, 0 ) and test( 1, bl ) as single parameters rather than as a pair of parameters. Of course, HLA does not consider commas (and bracketing symbols) within a string constant as the end of an actual parameter. So the following macro and invocation are perfectly legal:

#macro print( strToPrint );

     stdout.out( strToPrint );

#endmacro;
     .
     .
     .
     print( "Hello, world!" );

HLA treats the string Hello, world! as a single parameter because the comma appears inside a literal string constant, just as your intuition suggests.

If you are unfamiliar with textual macro parameter expansion in other languages, you should be aware that there are some problems you can run into when HLA expands your actual macro parameters. Consider the following macro declaration and invocation:

#macro Echo2nTimes( n, theStr );
     #for( echoCnt := 1 to n*2 )
          #print( theStr )
     #endfor
#endmacro;

     .
     .
     .
Echo2nTimes( 3+1, "Hello" );

This example displays Hello five times during compilation rather than the eight times you might intuitively expect. This is because the #for statement above expands to

#for( echoCnt := 1 to 3+1*2 )

The actual parameter for n is 3+1; because HLA expands this text directly in place of n, you get an erroneous text expansion. Of course, at compile time HLA computes 3+1*2 as the value 5 rather than as the value 8 (which you would get had HLA passed this parameter by value rather than by textual substitution).

The common solution to this problem when passing numeric parameters that may contain compile-time expressions is to surround the formal parameter in the macro with parentheses; for example, you would rewrite the macro above as follows:

#macro Echo2nTimes( n, theStr );

    #for( echoCnt := 1 to  (n)*2 )

        #print( theStr )

    #endfor

#endmacro;

The earlier invocation would expand to the following code:

#for( echoCnt := 1 to (3+1)*2 )
         #print( theStr )
    #endfor

This version of the macro produces the intuitive result.

If the number of actual parameters does not match the number of formal parameters, HLA will generate a diagnostic message during compilation. As with procedures, the number of actual parameters must agree with the number of formal parameters. If you would like to have optional macro parameters, then keep reading.

You may have noticed by now that some HLA macros don't require a fixed number of parameters. For example, the stdout.put macro in the HLA Standard Library allows one or more actual parameters. HLA uses a special array syntax to tell the compiler that you wish to allow a variable number of parameters in a macro parameter list. If you follow the last macro parameter in the formal parameter list with [ ], then HLA will allow a variable number of actual parameters (zero or more) in place of that formal parameter. For example:

#macro varParms( varying[] );

     << Macro body >>

#endmacro;
     .
     .
     .
     varParms( 1 );
     varParms( 1, 2 );
     varParms( 1, 2, 3 );
     varParms();

Note the last invocation especially. If a macro has any formal parameters, you must supply parentheses with the macro list after the macro invocation. This is true even if you supply zero actual parameters to a macro with a varying parameter list. Keep in mind this important difference between a macro with no parameters and a macro with a varying parameter list but no actual parameters.

When HLA encounters a formal macro parameter with the [ ] suffix (which must be the last parameter in the formal parameter list), HLA creates a constant string array and initializes that array with the text associated with the remaining actual parameters in the macro invocation. You can determine the number of actual parameters assigned to this array using the @elements compile-time function. For example, @elements( varying ) will return some value, 0 or greater, that specifies the total number of parameters associated with that parameter. The following declaration for varParms demonstrates how you might use this:

#macro varParms( varying[] );

     #for( vpCnt := 0 to @elements( varying ) - 1 )

          #print( varying[ vpCnt ] )

     #endfor

#endmacro;
     .
     .
     .
 varParms( 1 );        // Prints "1" during compilation.
 varParms( 1, 2 );     // Prints "1" and "2" on separate lines.
 varParms( 1, 2, 3 );  // Prints "1", "2", and "3" on separate lines.
 varParms();           // Doesn't print anything.

Because HLA doesn't allow arrays of text objects, the varying parameter must be an array of strings. This, unfortunately, means you must treat the varying parameters differently than you handle standard macro parameters. If you want some element of the varying string array to expand as text within the macro body, you can always use the @text function to achieve this. Conversely, if you want to use a nonvarying formal parameter as a string object, you can always use the @string( name ) function. The following example demonstrates this:

#macro ReqAndOpt( Required, optional[] );
     ?@text( optional[0] ) := @string( ReqAndOpt );
     #print( @text( optional[0] ))

     #endmacro;
     .
     .
     .
     ReqAndOpt( i, j );

// The macro invocation above expands to

     ?@text( "j" ) := @string( i );
     #print( "j" )

// The above further expands to

     j := "i";
     #print( j )

// The above simply prints "i" during compilation.

Of course, it would be a good idea, in a macro like the above, to verify that there are at least two parameters before attempting to reference element zero of the optional parameter. You can easily do this as follows:

#macro ReqAndOpt( Required, optional[] );

     #if( @elements( optional ) > 0 )

          ?@text( optional[0] ) := @string( ReqAndOpt );
          #print( @text( optional[0] ))

     #else

          #error( "ReqAndOpt must have at least two parameters" )

     #endif

#endmacro;

As the previous section notes, HLA requires exactly one actual parameter for each nonvarying formal macro parameter. If there is no varying macro parameter (and there can be at most one), then the number of actual parameters must exactly match the number of formal parameters. If a varying formal parameter is present, then there must be at least as many actual macro parameters as there are nonvarying (or required) formal macro parameters. If there is a single, varying actual parameter, then a macro invocation may have zero or more actual parameters.

There is one big difference between a macro invocation of a macro with no parameters and a macro invocation of a macro with a single, varying parameter that has no actual parameters: The macro with the varying parameter list must have an empty set of parentheses after it, while the macro invocation of the macro without any parameters does not allow this. You can use this fact to your advantage if you wish to write a macro that doesn't have any parameters but you want to follow the macro invocation with ( ) so that it matches the syntax of a procedure call with no parameters. Consider the following macro:

#macro neg64( JustForTheParens[] );

     #if( @elements( JustForTheParens ) = 0 )

          neg( edx );
          neg( eax );
          sbb( 0, edx );

     #else

          #error( "Unexpected operand(s)" )

     #endif

#endmacro;

The preceding macro requires invocations of the form neg64(); to use the same syntax you would use for a procedure call. This feature is useful if you want the syntax of your parameterless macro invocations to match the syntax of a parameterless procedure call. It's not a bad idea to do this, just in the off chance you need to convert the macro to a procedure at some point (or vice versa, for that matter).

Consider the following macro declaration:

macro JZC( target );

         jnz NotTarget;
         jc target;
     NotTarget:

endmacro;

The purpose of this macro is to simulate an instruction that jumps to the specified target location if the zero flag is set and the carry flag is set. Conversely, if either the zero flag is clear or the carry flag is clear, this macro transfers control to the instruction immediately following the macro invocation.

There is a serious problem with this macro. Consider what happens if you use this macro more than once in your program:

JZC( Dest1 );
          .
          .
          .
     JZC( Dest2 );
          .
          .
          .

The preceding macro invocations expand to the following code:

jnz NotTarget;
    jc Dest1;
NotTarget:
         .
         .
         .
    jnz NotTarget;
    jc Dest2;
NotTarget:
         .
         .
         .

The problem with the expansion of these two macro invocations is that they both emit the same label, NotTarget, during macro expansion. When HLA processes this code it will complain about a duplicate symbol definition. Therefore, you must take care when defining symbols inside a macro because multiple invocations of that macro may lead to multiple definitions of that symbol.

HLA's solution to this problem is to allow the use of local symbols within a macro. Local macro symbols are unique to a specific invocation of a macro. For example, had NotTarget been a local symbol in the preceding JZC macro invocations, the program would have compiled properly because HLA treats each occurrence of NotTarget as a unique symbol.

HLA does not automatically make internal macro symbol definitions local to that macro.[123] Instead, you must explicitly tell HLA which symbols must be local. You do this in a macro declaration using the following generic syntax:

#macro macroname( optional_parameters ):optional_list_of_local_names ;
     << Macro body >>
#endmacro;

The list of local names is a sequence of one or more HLA identifiers separated by commas. Whenever HLA encounters this name in a particular macro invocation, it automatically substitutes some unique name for that identifier. For each macro invocation, HLA substitutes a different name for the local symbol.

You can correct the problem with the JZC macro by using the following macro code:

#macro JZC( target ):NotTarget;

         jnz NotTarget;
         jc target;
     NotTarget:

#endmacro;

Now whenever HLA processes this macro it will automatically associate a unique symbol with each occurrence of NotTarget. This will prevent the duplicate-symbol error that occurs if you do not declare NotTarget as a local symbol.

HLA implements local symbols by substituting a symbol like _nnnn_ (where nnnn is a four-digit hexadecimal number) wherever the local symbol appears in a macro invocation. For example, a macro invocation of the form JZC( SomeLabel ); might expand to

jnz _010A_;
      jc SomeLabel;
_010A_:

For each local symbol appearing within a macro expansion, HLA will generate a unique temporary identifier by simply incrementing this numeric value for each new local symbol it needs. As long as you do not explicitly create labels of the form _nnnn_Text_ (where nnnn is a hexadecimal value), there will never be a conflict in your program. HLA explicitly reserves all symbols that begin and end with a single underscore for its own private use (and for use by the HLA Standard Library). As long as you honor this restriction, there should be no conflicts between HLA local symbol generation and labels in your own programs because all HLA-generated symbols begin and end with a single underscore.

HLA implements local symbols by effectively converting that local symbol to a text constant that expands to the unique symbol HLA generates for the local label. That is, HLA effectively treats local symbol declarations as indicated by the following example:

#macro JZC( target );
     ?NotTarget:text := "_010A_Text_";

         jnz NotTarget;
         jc target;

     NotTarget:

#endmacro;

Whenever HLA expands this macro it will substitute _010A_Text_ for each occurrence of NotTarget it encounters in the expansion. This analogy isn't perfect because the text symbol NotTarget in this example is still accessible after the macro expansion, whereas this is not the case when defining local symbols within a macro. But this does give you an idea of how HLA implements local symbols.

Although programmers typically use macros to expand to some sequence of machine instructions, there is absolutely no requirement that a macro body contain any executable instructions. Indeed, many macros contain only compile-time language statements (for example, #if, #while, #for, ? assignments, and the like). By placing only compile-time language statements in the body of a macro, you can effectively write compile-time procedures and functions using macros.

The following unique macro is a good example of a compile-time function that returns a string result. Consider the definition of this macro:

#macro unique:theSym;
     @string(theSym)
#endmacro;

Whenever your code references this macro, HLA replaces the macro invocation with the text @string(theSym), which, of course, expands to some string like _021F_Text_. Therefore, you can think of this macro as a compile-time function that returns a string result.

Be careful that you don't take the function analogy too far. Remember, macros always expand to their body text at the point of invocation. Some expansions may not be legal at any arbitrary point in your programs. Fortunately, most compile-time statements are legal anywhere whitespace is legal in your programs. Therefore, macros behave as you would expect functions or procedures to behave during the execution of your compile-time programs.

Of course, the only difference between a procedure and a function is that a function returns some explicit value, while procedures simply do some activity. There is no special syntax for specifying a compile-time function return value. As the example above indicates, simply specifying the value you wish to return as a statement in the macro body suffices. A compile-time procedure, on the other hand, would not contain any non-compile-time language statements that expand into some sort of data during macro invocation.

The C++ language supports a nifty feature known as function overloading. Function overloading lets you write several different functions or procedures that all have the same name. The difference between these functions is the types of their parameters or the number of parameters. A procedure declaration is unique in C++ if it has a different number of parameters than other functions with the same name or if the types of its parameters differ from other functions with the same name. HLA does not directly support procedure overloading, but you can use macros to achieve the same result. This section explains how to use HLA's macros and the compile-time language to achieve function/procedure overloading.

One good use for procedure overloading is to reduce the number of Standard Library routines you must remember how to use. For example, the HLA Standard Library provides five different "puti" routines that output an integer value: stdout.puti128, stdout.puti64, stdout.puti32, stdout.puti16, and stdout.puti8. The different routines, as their names suggest, output integer values according to the size of their integer parameter. In the C++ language (or another other language supporting procedure/function overloading) the engineer designing the input routines would probably have chosen to name them all stdout.puti and leave it up to the compiler to select the appropriate one based on the operand size.[124] The macro in Example 9-5 demonstrates how to do this in HLA using the compile-time language to figure out the size of the parameter operand.

Example 9-5. Simple procedure overloading based on operand size

// Puti.hla
//
// This program demonstrates procedure overloading via macros.
//
// It defines a "puti" macro that calls stdout.puti8, stdout.puti16,
// stdout.puti32, or stdout.puti64, depending on the size of
// the operand.

program putiDemo;
#include( "stdlib.hhf" )


// puti-
//
// Automatically decides whether we have a 64-, 32-, 16-, or 8-bit
// operand and calls the appropriate stdout.putiX routine to
// output this value.

#macro puti( operand );

     // If we have an 8-byte operand, call puti64:

     #if( @size( operand ) = 8 )

          stdout.puti64( operand );


     // If we have a 5-byte operand, call puti32:

     #elseif( @size( operand ) = 4 )

          stdout.puti32( operand );


     // If we have a 2-byte operand, call puti16:

     #elseif( @size( operand ) = 2 )

          stdout.puti16( operand );


     // If we have a 1-byte operand, call puti8:

     #elseif( @size( operand ) = 1 )

          stdout.puti8( operand );


     // If it's not an 8-, 4-, 2-, or 1-byte operand,
     // then print an error message:

     #else

          #error( "Expected a 64-, 32-, 16-, or 8-bit operand" )

     #endif

#endmacro;


// Some sample variable declarations so we can test the macro above:

static
     i8:  int8    := −8;
     i16: int16   := −16;
     i32: int32   := −32;
     i64: qword;


begin putiDemo;

     // Initialize i64 because we can't do this in the static section.

     mov( −64, (type dword i64 ));
     mov( $FFFF_FFFF, (type dword i64[4]));

     // Demo the puti macro:

     puti( i8  ); stdout.newln();
     puti( i16 ); stdout.newln();
     puti( i32 ); stdout.newln();
     puti( i64 ); stdout.newln();

end putiDemo;

The example above simply tests the size of the operand to determine which output routine to use. You can use other HLA compile-time functions, such as @typename, to do more sophisticated processing. Consider the program in Example 9-6, which demonstrates a macro that overloads stdout.puti32, stdout.putu32, and stdout.putd depending on the type of the operand.

You can easily extend this macro to output 8- and 16-bit operands as well as 32-bit operands. That is left as an exercise for the reader.

The number of actual parameters is another way to resolve which overloaded procedure to call. If you specify a variable number of macro parameters (using the [ ] syntax; see the discussion in 9.8.2.2 Macros with a Variable Number of Parameters), you can use the @elements compile-time function to determine exactly how many parameters are present and call the appropriate routine. The sample in Example 9-7 uses this trick to determine whether it should call stdout.puti32 or stdout.puti32Size.

All the examples up to this point provide procedure overloading for Standard Library routines (specifically, the integer output routines). Of course, you are not limited to overloading procedures in the HLA Standard Library. You can create your own overloaded procedures as well. All you have to do is write a set of procedures, all with unique names, and then use a single macro to decide which routine to actually call based on the macro's parameters. Rather than call the individual routines, invoke the common macro and let it decide which procedure to actually call.



[122] To differentiate between macros and procedures, this text will use the term invocation when describing the use of a macro and call when describing the use of a procedure.

[123] Sometimes you actually want the symbols to be global.

[124] By the way, the HLA Standard Library does this as well. Although it doesn't provide stdout.puti, it does provide stdout.put, which will choose an appropriate output routine based upon the parameter's type. This is a bit more flexible than a puti routine.