7.6 Compile-Time Functions
HLA provides a wide range of compile-time functions you can use. These functions compute values during compilation the same way a high level language function computes values at run-time. The HLA compile-time language includes a wide variety of numeric, string, and symbol table functions that help you write sophisticated compile-time programs.
Most of the names of the built-in compile-time functions begin with the special symbol "@" and have names like @sin or @length. The use of these special identifiers prevents conflicts with common names you might want to use in your own programs (like length). The remaining compile-time functions (those that do not begin with "@") are typically data conversion functions that use type names like int8 and real64. You can even create your own compile-time functions using macros (see "Macros" on page 969).
HLA organizes the compile-time functions into various classes depending on the type of operation. For example, there are functions that convert constants from one form to another (e.g., string to integer conversion), there are many useful string functions, and HLA provides a full set of compile-time numeric functions.
The complete list of HLA compile-time functions is too lengthy to present here. Instead, a complete description of each of the compile-time objects and functions appears in Appendix H (see "HLA Compile-Time Functions" on page 1493); this section will highlight a few of the functions in order to demonstrate their use. Later sections in this chapter, as well as future chapters, will make extensive use of the various compile-time functions.
Perhaps the most important concept to understand about the compile-time functions is that they are equivalent to constants in your assembly language code (i.e., the run-time program). For example, the compile-time function invocation "@sin( 3.1415265358979328 )" is roughly equivalent to specifying "0.0" at that point in your program1. A function invocation like "@sin( x )" is legal only if x is a constant with a previous declaration at the point of the function call in the source file. In particular, x cannot be a run-time variable or other object whose value exists at run-time rather than compile-time. Since HLA replaces compile-time function calls with their constant result, you may ask why you should even bother with compile time functions. After all, it's probably more convenient to type "0.0" than it is to type "@sin( 3.1415265358979328 )" in your program. However, compile-time functions are really handy for generating lookup tables (see "Generating Tables" on page 651) and other mathematical results that may change whenever you change a CONST value in your program. Later sections in this chapter will explore these ideas farther.
7.6.1 Type Conversion Compile-time Functions
One set of commonly used compile-time functions are the type conversion functions. These functions take a single parameter of one type and convert that information to some specified type. These functions use several of the HLA built-in data type names as the function names. Functions in this category are
- int8, int16, and int32
- uns8, uns16, and uns32
- byte, word, dword (these are effectively equivalent to uns8, uns16, and uns32)
- real32, real64, and real80
These functions accept a single constant expression parameter and, if at all reasonable, convert that expression's value to the type specified by the type name. For example, the following function call returns the value -128 since it converts the string constant to the corresponding integer value:int8( "-128" )
Certain conversions don't make sense or have restrictions associated with them. For example, the boolean function will accept a string parameter, but that string must be "true" or "false" or the function will generate a compile-time error. Likewise, the numeric conversion functions (e.g., int8) allow a string operand but the string operand must represent a legal numeric value. Some conversions (e.g., int8 with a character set parameter) simply don't make sense and are always illegal.
One of the most useful functions in this category is the string function. This function accepts nearly all constant expression types and it generates a string that represents the parameter's data. For example, the invocation "string( 128 )" produces the string "128" as the return result. This function is real handy when you have a value that you wish to use where HLA requires a string. For example, the #ERROR compile-time statement only allows a single string operand. You can use the string function and the string concatenation operator ("+") to easily get around this limitation, e.g.,#error( "Value (" + string( Value ) + ") is out of range" )
7.6.2 Numeric Compile-Time Functions
The functions in this category perform standard mathematical operations at compile time. These functions are real handy for generating lookup tables and "parameterizing" your source code by recalculating functions on constants defined at the beginning of your program. Functions in this category include the following:
See Appendix H for more details on these functions.
7.6.3 Character Classification Compile-Time Functions
The functions in this group all return a boolean result. They test a character (or all the characters in a string) to see if it belongs to a certain class of characters. The functions in this category include
In addition to these character classification functions, the HLA language provides a set of pattern matching functions that you can also use to classify character and string data. See the appropriate sections a little later for the discussion of these routines.
7.6.4 Compile-Time String Functions
The functions in this category operate on string parameters. Most return a string result although a few (e.g., @length and @index) return integer results. These functions do not directly affect the values of their parameters; instead, they return an appropriate result that you can assign back to the parameter if you wish to do so.
- @delete, @insert
- @index, @rindex
- @lowercase, @uppercase
- @strbrk, @strspan
- @substr, @tokenize, @trim
For specific details concerning these functions and their parameters and their types, see Appendix H. Combined with the pattern matching functions, the string handling functions give you the ability to extend the HLA language by processing textual data that appears in your source files. Later sections appearing in this chapter will discuss ways to do this.
The @length function deserves a special discussion because it is probably the most popular function in this category. It returns an uns32 constant specifying the number of characters found in its string parameter. The syntax is the following:@length( string_expression )
Where string_expression represents any compile-time string expression. As noted above, this function returns the length, in characters, of the specified expression.
7.6.5 Compile-Time Pattern Matching Functions
HLA provides a very rich set of string/pattern matching functions that let you test a string to see if it begins with certain types of characters or strings. Along with the string processing functions, the pattern matching functions let you extend the HLA language and provide several other benefits as well. There are far too many pattern matching functions to list here (see Appendix H for complete details). However, a few examples will demonstrate the power and convenience of these routines.
The pattern matching functions all return a boolean true/false result. If a function returns true, we say that the function succeeds in matching its operand. If the function returns false, then we say it fails to match its operand. An important feature of the pattern matching functions is that they do not have to match the entire string you supply as a parameter, these patterns will (usually) succeed as long as they match a prefix of the string parameter. The @matchStr function is a good example, the following function invocation always returns true:@matchStr( "Hello World", "Hello" )
The first parameter of all the pattern matching functions ("Hello World" in this example) is the string to match. The matching functions will attempt to match the characters at the beginning of the string with the other parameters supplied for that particular function. In the @matchStr example above, the function succeeds if the first parameter begins with the string specified as the second parameter (which it does). The fact that the "Hello World" string contains additional characters beyond "Hello" is irrelevant; it only needs to begin with the string "Hello" is doesn't require equality with "Hello".
Most of the compile-time pattern matching functions support two optional parameters. The functions store additional data into the VAL objects specified by these two parameters if the function is successful (conversely, if the function fails, it does not modify these objects). The first parameter is where the function stores the remainder. The remainder after the execution of a pattern matching function is those characters that follow the matched characters in the string. In the example above, the remainder would be " World". If you wanted to capture this remainder data, you would add a third parameter to the @matchStr function invocation:@matchStr( "Hello World", "Hello", World )
This function invocation would leave " World" sitting in the World VAL object. Note that World must be predeclared as a string in the VAL section (or via the "?" statement) prior to the invocation of this function.
By using the conjunction operator ("&") you can combine several pattern matching functions into a single expression, e.g.,@matchStr( "Hello There World", "Hello ", tw ) & @matchStr( tw, "There ", World )
This full expression returns true and leaves "World" sitting in the World variable. It also leaves "There World" sitting in tw, although tw is probably a temporary object whose value has no meaning beyond this expression. Of course, the above could be more efficiently implemented as follows:@matchStr( "Hello There World", "Hello There", World )
However, keep in mind that you can combine different pattern matching functions using conjunction, they needn't all be calls to @matchStr.
The second optional parameter to most pattern matching functions holds a copy of the text that the function matched. E.g., the following call to @matchStr returns "Hello" in the Hello VAL object2@matchStr( "Hello World", "Hello", World, Hello )
For more information on these pattern matching functions please see Appendix H. The chapter on Domain Specific Languages (see "Domain Specific Embedded Languages" on page 1003) and several other sections in this chapter will also make further use of these functions.
7.6.6 Compile-Time Symbol Information
During compilation HLA maintains an internal database known as the symbol table. The symbol table contains lots of useful information concerning all the identifiers you've defined up to a given point in the program. In order to generate machine code output, HLA needs to query this database to determine how to treat certain symbols. In your compile-time programs, it is often necessary to query the symbol table to determine how to handle an identifier or expression in your code. The HLA compile-time symbol information functions handle this task.
Many of the compile-time symbol information functions are well beyond the scope of this chapter (and, in some cases, beyond the scope of this text). This chapter will present a few of the functions and later chapters will add to this list. For a complete list of the compile-time symbol table functions, see Appendix H. The functions we will consider in this chapter include the following:
Without question, the @size function is probably the most important function in this group. Indeed, previous chapters have made use of this function already. The @size function accepts a single HLA identifier or constant expression as a parameter. It returns the size, in bytes, of the data type of that object (or expression). If you supply an identifier, it can be a constant, type, or variable identifier. HLA returns the size of the type of the object. As you've seen in previous chapters, this function is invaluable for allocating storage via malloc and allocating arrays.
Another very useful function in this group is the @defined function. This function accepts a single HLA identifier as a parameter, e.g.,@defined( MyIdentifier )
This function returns true if the identifier is defined at that point in the program, it returns false otherwise.
The @typeName function returns a string specifying the type name of the identifier or expression you supply as a parameter. For example, if i32 is an int32 object, then "@typeName( i32 )" returns the string "int32". This function is useful for testing the types of objects you are processing in your compile-time programs.
The @elements function requires an array identifier or expression. It returns the total number of array elements as the function result. Note that for multi-dimensional arrays this function returns the product of all the array dimensions3.
The @elementSize function returns the size, in bytes, of an element of an array whose name you pass as a parameter. This function is extremely valuable for computing indices into an array (i.e., this function computes the element_size component of the array index calculation, see "Accessing Elements of a Single Dimension Array" on page 465).
7.6.7 Compile-Time Expression Classification Functions
The HLA compile-time language provides functions that will classify some arbitrary text and determine if that text is a constant expression, a register, a memory operand, a type identifier, and more. Some of the more common functions are
Except for @isType, which requires an HLA identifier as a parameter, these functions all take some arbitrary text as their parameter. These functions return true or false depending upon whether that parameter satisfies the function requirements (e.g., @isConst returns true if its parameter is a constant identifier or expression). The @isType function returns true if its parameter is a type identifier.
The HLA compile-time language includes several other classification functions that are beyond the scope of this chapter. See Appendix H for details on those functions.
7.6.8 Miscellaneous Compile-Time Functions
The HLA compile-time language contains several additional functions that don't fall into one of the categories above. Some of the more useful miscellaneous functions include
The @odd function takes an ordinal value (i.e., non-real numeric or character) as a parameter and returns true if the value is odd, false if it is even. The @lineNumber function requires no parameters, it returns the current line number in the source file. This function is quite useful for debugging compile-time (and run-time!) programs.
The @text function is probably the most useful function in this group. It requires a single string parameter. It expands that string as text in place of the @text function call. This function is quite useful in conjunction with the compile-time string processing functions. You can build an instruction (or a portion of an instruction) using the string manipulation functions and then convert that string to program source code using the @text function. The following is a trivial example of this function in operation:?id1:string := "eax"; ?id2:string := "i32"; @text( "mov( " + id1 + ", " + id2 + ");" )
The sequence above compiles tomov( eax, i32 );
7.6.9 Predefined Compile-Time Variables
In addition to functions, HLA also includes several predefined compile-time variables. The use of most of HLA's compile time variables is beyond the scope of this text. However, the following you've already seen:
Volume Three (see "Some Additional Instructions: INTMUL, BOUND, INTO" on page 393) discusses the use of these objects to control the emission of the INTO and BOUND instructions. These two boolean pseudo-variables determine whether HLA will compile the BOUND (@bound) and INTO (@into) instructions or treat them as comments. By default, these two variables contain true and HLA will compile these instructions to machine code. However, if you set these values to false, using one or both of the following statements then HLA will not compile the associated statement:?@bound := false; ?@into := false;
If you set @BOUND to false, then HLA treats BOUND instructions as though they were comments. If you set @INTO to false, then HLA treats INTO instructions as comments. You can control the emission of these statements throughout your program by selectively setting these pseudo-variables to true or false at different points in your code.
7.6.10 Compile-Time Type Conversions of TEXT Objects
Once you create a text constant in your program, it's difficult to manipulate that object. The following example demonstrates a programmer's desire to change the definition of a text symbol within a program:val t:text := "stdout.put"; . . . ?t:text := "fileio.put";
The basic idea in this example is that t expands to "stdout.put" in the first half of the code and it expands to "fileio.put" in the second half of the program. Unfortunately, this simple example will not work. The problem is that HLA will expand a text symbol in place almost anywhere it finds that symbol. This includes occurrences of t within the "?" statement above. Therefore, the code above expands to the following (incorrect) text:val t:text := "stdout.put"; . . . ?stdout.put:text := "fileio.put";
HLA doesn't know how to deal with the "?" statement above, so it generates a syntax error.
At times you may not want HLA to expand a text object. Your code may want to process the string data held by the text object. HLA provides a couple of operators that deal with these two problems:
The @string:identifier operator consists of @string, immediately followed by a colon and a text identifier (with no interleaving spaces or other characters). HLA returns a string constant corresponding to the text data associated with the text object. In other words, this operator lets you treat a text object as though it were a string constant within an expression.
Unfortunately, the @string operator converts a text object to a string constant, not a string identifier. Therefore, you cannot say something like?@string:t := "Hello"
This doesn't work because @string:t replaces itself with the string constant associated with the text object t. Given the former assignment to t, this statement expands to?"stdout.put" := "Hello";
This statement is still illegal.
The @toString:identifier operator comes to the rescue in this case. The @toString operator requires a text object as the associated identifier. It converts this text object to a string object (still maintaining the same string data) and then returns the identifier. Since the identifier is now a string object, you can assign a value to it (and change its type to something else, e.g., text, if that's what you need). To achieve the original goal, therefore, you'd use code like the following:val t:text := "stdout.put"; . . . ?@tostring:t : text := "fileio.put";
1Actually, since @sin's parameter in this example is not exactly ¼, you will get a small positive number instead of zero as the function result, but in theory you should get zero.
2Strictly speaking, this example is rather contrived since we generally know the string that @matchStr matches. However, for other pattern matching functions this is not the case.
3There is an @dim function that returns an array specifying the bounds on each dimension of a multidimensional array. See the appendices for more details if you're interested in this function.