Preprocessor

TADS 3 provides a number of extensions to the TADS 2 preprocessor, for greater power and more compatibility with C and C++ preprocessors.

Macro substitution parameters

You can now define substitution parameters in your macros.  These parameters, when they appear in the expansion text, are replaced during preprocessing with text specified in the macro's invocation.  For example, suppose we define this macro:

 

#define ERROR(msg)  tadsSay('An error occurred: ' + msg + '\n')

 

Now suppose we write this in our code somewhere:

 

   ERROR('invalid value');

 

During compilation, the preprocessor will expand this macro invocation, substituting the actual parameter value when msg appears in the replacement text.  The resulting expansion is:

 

   tadsSay('An error occurred: ' + 'invalid value' + '\n');

 

(It is worth pointing out that the compiler will subsequently compute the constant value of this string concatenation, so this will not result in any string concatenation at run-time.)

 

The TADS 3 preprocessor uses the ANSI C rules for macro expansion with regards to recursive macro invocation and circular definitions.  These rules are complex and the need to know them arises quite infrequently, so it is not worth trying to explain them here; authors who are curious should refer to a good ANSI C programming book for the details.

Stringizing

It is sometimes useful to write a macro that uses the actual text of a substitution parameter as a string constant.  This can be accomplished using the "stringizing" operators.  The # operator, when it precedes the name of a macro formal parameter in macro expansion text, is replaced by the text of the actual argument value enclosed in double quotes.  The #@ operator has a similar effect, but encloses the text in single quotes.  For example, suppose we wanted to write a debugging macro that displays the value of an arbitrary expression:

 

#define printval(val) tadsSay(#@val + ' = ' + toString(val))

 

We could use this macro in our code like this:

 

    printval(MyObject.codeNum);

 

This would expand as follows:

 

    tadsSay('MyObject.codeNum' + ' = ' + toString(MyObject.codeNum));

Token Pasting

In some cases, it is useful to be able to construct a new symbol out of different parts.  This can be accomplished with "token pasting," which constructs a single token from what were originally several tokens.  The token pasting operator, ##, when it appears in a macro's expansion text, takes the text of the token to the left of the operator and the text of the token to the right of the operator and pastes them together to form a single token.  If the token on either side is a formal parameter to the macro, the operator first expands the formal parameter, then performs pasting on the result.

 

For example, suppose we wanted to construct a method call based on a partial method name:

 

#define callDo(verb, actor)  do##verb(actor)

 

We could use the macro like this:

 

   dobj.callDo(Take, Me);

 

This would expand into this text:

 

   dobj.doTake(Me);

 

The preprocessor scans a pasted token for further expansion, so if the pasted token is itself another macro, the preprocessor expands that as well:

 

#define PASTE(a, b) a##b
#define FOOBAR 123
PASTE(FOO, BAR)

 

The macro above expands as follows.  First, the preprocessor expands the PASTE macro, pasting the two arguments together to yield the token FOOBAR.  The preprocessor then scans that and finds that it's another macro, so it expands it.  The final text is simply 123.

 

Token pasting only works within macro expansion text; the token pasting operator is ignored if it appears anywhere outside of a #define.

String Concatenation

When you use the ## operator to paste two tokens together, the preprocessor checks to see if both of the tokens being pasted together are strings of the same kind (i.e., they both have the same type of quotes).  If they are, the preprocessor combines the strings by removing the closing quote of the first string and the opening quote of the second string.

 

If either operand of the ## operator is itself modified by the # operator, the preprocessor first applies the # operator or operators, and then applies the ## operator.  So, if you paste together two stringized parameters, the result is a single string.

 

Here are some examples:

 

#define PAREN_STR(a) "(" ## a ")"
#define CONCAT(a, b) a ## b
#define CONCAT_STR(a, b) #a ## #b
#define DEBUG_PRINT(a) "value of " ## #a ## " = <<a>>"
 
1: PAREN_STR("parens")
2: CONCAT("abc", "def")
3: CONCAT(uvw, xyz)
4: DEBUG_PRINT(obj.prop[3])

 

After preprocessing, the file above would appear as follows:

 

1: "(parens)"
2: "abcdef"
3: "uvwxyz"
4: "value of obj.prop[3] = <<obj.prop[3]>>"

 

Note that string concatenation is a TADS extension, and is not found in ANSI C preprocessors.  The C preprocessor doesn't provide a way of combining string tokens because the C language (not the preprocessor, but the language itself) has a different way of accomplishing the same thing: in C, two adjacent string tokens are always treated as a single string formed by concatenating the two strings together.  The TADS language doesn't allow this kind of implicit string pasting, because (unlike in C) there are times when it is valid to use two or more adjacent string tokens, such as in dictionary property lists.  The TADS preprocessor therefore provides its own mechanism for concatenating string tokens.

Conditionals: #if and #elif

The TADS 3 preprocessor supports the C-style #if and #elif directives.  These directives let you specify sections of text to be compiled or omitted conditionally, based on the value of a constant expression.  (TADS 2 supported conditional compilation, but only through the #ifdef and #ifndef directives, which could only determine if a preprocessor symbol was defined or not.)  These new directives work the same as they do in ANSI C: the preprocessor expands macros in the argument and evaluates the result as a constant expression; a non-zero value is considered true.  Here's an example that checks the version of an included library header to make sure it's recent enough:

 

#include "MyLib.h"
#if MYLIB_VSN < 5
#error "This module requires MyLib version 5 or higher."
#endif

 

Note that the defined() preprocessor operator can be used within an expression in these directives to test to determine if a preprocessor symbol is defined.  This allows for tests of combinations of defined symbols:

 

#if defined(MSDOS) || defined(AMIGA) || defined(UNIX)

Improved include file searching

The #include directive uses an improved algorithm to search for included files; the new algorithm is essentially the same as that used by most C compilers.  For files whose names are specified in double quotes, the compiler first looks in the directory containing the including file, then in the directory containing the file that included the including file, and so on until it reaches the original source file; if none of these directories contain the file then the search proceeds as for angle-bracketed files.  For angle-bracketed files, the compiler searches in each directory specified in the include path (specified with the t3make –I option), in the order specified by the user (i.e., in the order in which the -I options appear).  Finally, the compiler searches in the compiler directory itself.  Note that none of this applies to a file with an absolute path name specified; if an absolute path is specified, the compiler only looks in the actual path given.

#include filename macro expansion

The #include directive expands any macros that appear in the filename, if the filename is not enclosed in quotes or angle brackets.  The result of expanding any macros must yield a string delimited in quotes or angle brackets.  (This feature is obscure, but TADS 3 includes it for completeness of its ANSI C preprocessor compatibility.)

The #line Directive

The preprocessor supports the C-style #line directive, which lets you override the compiler's internal notion of the current source filename and line number.  This feature is probably of little use to most game and library authors, since the compiler automatically keeps track of the actual filename and line number as it processes the source code.

 

The #line directive will be of greatest interest to tool writers.  If you're writing a tool that adds an extra stage of preprocessing before the TADS 3 compiler sees the source code, you can use #line to specify the original source location for generated code.  This will allow the compiler to generate error messages that relate back to the original source code, and will allow the debugger to display the original source code rather than the generated TADS code.