Chapter 7
Procedural Macro Implementation and State Management

In order to use procedural macros effectively, it is necessary to know a little bit about how they are implemented. This section gives the details on how procedural macros are implemented, the use of macro libraries, and how to share state between procedural macros.

7.1 The Details

The current ZL compiler does not contain an interpreter; thus procedural macros are compiled and then dynamically linked into the compiler when the macro is first used. A simple dependency analysis is done so that any components that the procedural macro depends on (and are not already compiled and linked in) are also compiled at the same time.

In addition, ZL determines the role of each function as for run-time or compile-time only to avoid included macro related functions in the executable. A compile-time only function is any function that uses part of a macro API, or one that depends on a function which does.

The dependency analysis that determines which code to include when a procedural macro is first used is separate from the dependency analysis used to determine a role. Thus, it is possible for a function to be used at both run-time and compile-time if the function is used by both a normal (i.e., run-time) function and a compile-time only function. Such a function will be considered a run-time function even though it is also used at compile time.

7.2 Macro Libraries

Since the compilation of a complicated procedural macro can take a decent amount of time, ZL also provides a mechanism for precompiling macros ahead of time via macro libraries. A macro library is similar to a normal library, except that the code is loaded while compiling the program, instead of during the programs execution.

A macro library is a collection of code compiled with the -C option. The compilation creates a shared library with the -fct.so extension; for example, if the code for the library was contained in the file lib.zl, the shared library will be called lib-fct.so. The macro library is then used by importing the same file (used to create the library) using the import_file primitive. Importing will: 1) parse enough of the macro library code to get the function prototypes and related information; and 2) load the related shared library. A header file can also be provided (with an extension of .zlh), which will be read in instead of the full macro code.

Normally, when new_mark() (which uses the environ_snapshot() primitive) is used, the environmental snapshot is taken at the place in the code where the syntax is used. (Basically, environ_snapshot() gets replaced with a pointer the the current environment as the procedural macro is being parsed.) Unfortunately, ZL does not have the ability to serialize the environment, which means a snapshots can only be taken for code that is compiled in the same translation unit (also known as the compilation unit). This creates a problem when a procedural macro is compiled into a library. To work around this problem the user can declare that the environmental snapshot is taken where the macro is declared, rather then where environ_snapshot() is used, by adding :w_snapshot to make_macro, for example:

  make_syntax_macro foreach :w_snapshot

Since, unlike the function body, the make_macro declaration is always read as the program is being compiled, this ensures that there is always a point where the snapshot can be taken. In the rare cases when this strategy will not work, it is possible to store a snapshot of an environment in a variable. For example, if:

  EnvironSnapshot * prelude_envss = environ_snapshot();

is found in a header file, than ZL will ensure that the value of global variable prelude_envss is a pointer to an environmental snapshot in the current compilation unit. Within the macro library, this variable can then be used with an alternative form of new_mark, which accepts a pointer to EnvironSnapshot as its first parameter.

When macro libraries are used no automatic dependency analysis is done; everything included in the macro library is assumed to used at compile-time only. If it is necessary to use the same code at both compile-time and run-time, special previsions need to be made, such as moving the shared code into a separate file so that it can be linked in at both compile and run time. Linking compile-time only functions into the executable will fail with undefined symbols.

7.3 State Management

Macros may maintain global state in one of two ways. The first way is to simply use global variables; any state stored within a global variable will be accessible to any macros used in the same compilation, even if they are compiled and linked in separately. The other way to maintain global state is to store the information inside of a top-level symbol via the use of symbol properties, the details of which are provided in the next section.

Using either method, state is only maintained during within the compilation unit. Separate provisions need to be made to store state between compilations.

7.4 Symbol Properties

Any top-level symbol can have any number of properties associated with it. The value of the propriety is simply a syntax object. Symbol properties are used extensively by the class macro to store information about the class which is then used by the parent class and when expanding method definitions defined outside of the class.

Figure 7.1 shows the syntax for the add_prop primitive used for adding symbol properties.


Syntax to add properties to existing symbols:
  (add_prop SYMBOL PROPERTY-NAME VALUE)

Syntax to add properties within modules:
  (add_prop PROPERTY-NAME VALUE)

Macro API function to retrieve properties:
  Syntax * get_symbol_prop(UnmarkedSyntax * symbol, UnmarkedSyntax * prop,
                           const Environ *)

Figure 7.1: Symbol properties syntax and API.


Note that add_prop is not an API function; it is part of the syntax returned by the macro. In addition, the add_prop primitive is always used in the lower level s-expression form (i.e., created using raw_syntax instead of syntax) in order to be able to precisely control the syntax object being added. Such control would not be possible in the higher level syntax due to reparsing.

The three argument form of add_prop is used to add properties to already existing top-level symbols. For example the class macro adds the propriety is_method to the macro representing methods by using:

  (add_prop (fun method (. @parms)) is_method true)

where method and @parms are pattern variables. The two argument form of add_prop is used within a module or user type to add properties to the module.

To retrieve properties from a symbol the macro API function get_symbol_prop can be used. The function will return NULL if the property does not exist for that symbol.

When used in combination with stash_ptr and extract_ptr arbitrary objects can be stashed away for latter retrieval. For example the class macro uses this to store a pointer to the class used to implement the class in the module for the class. This pointer is then extracted when expanding method definitions defined outside of the class, thus greatly simplifying the implementation.

Converted From LaTeX using TeX4ht. PDF Version