Skip to content

Macro system

Macro system enables TML programs to write themselves using existing language constructs and simple parametrization. Unlike C, TML macros work on AST level (instead of the level of pure source code text). This makes them safe to use, as TML macros are fully analyzed syntactically and semantically before expansion. Using guard blocks is preferred, if possible, as they support combinatorial branch analysis. Macro system does not analyze discarded branches. Macro statements are inlined after expansion and new lexical scope is not created. Users must pay special attention to variable naming and shadowing after macro expansion.

Order of TML parsing and macro analysis and expansion is:

  1. TML source code is parsed (syntax analysis). Syntax errors are reported for both macro and non-macro code.
  2. Macro blocks are analyzed. Semantic errors related to macro blocks are reported.
  3. Macro blocks are expanded. Portions of code are either duplicated or discarded.
  4. Ordinary TML analyses are performed over new AST. Analyses do not care whether code was manually written or generated using macro system.

Macro statements can be arbitrarily nested and combined. Macro statements can be placed in both local and global scope. Ordinary rules for statements are applied (e.g. only declarations are allowed in global scope). Macro system does not allow arbitrary placement of statements in places in which they are forbidden by the rules of the language. Macro system does not currently support parametric generation of functions.

Macro system enables TML components to:

  • support dynamic terminal groups of non-uniform type, by expanding logic for each terminal, regardless of their number
  • support for components with vastly different logic depending on a property in a single source code file
  • support for manual loop unrolling and branch pruning (not advised as it can slow down compilation and worsen performance)

Macro indexing

Terminal indexing is the simplest type of macro expression. In contrast to dynamic indexing of arbitrary TML expressions, macro terminal indexing uses curly braces ({}). Macro indices can be arbitrary expressions of type int that can be statically evaluated. Macro indexing supports terminals and user variables, but treats them differently.

Static evaluator limitations

Static evaluator currently supports only simple arithmetic, relational and logical expressions. Errors will be reported if expression cannot be evaluated statically. Support for expressions will be expaded as needed, but the general rule for writing maintainable code is that static indexing expressions should be as simple as possible. Static expressions are not allowed to have side effects, so arbitrary usage of static expressions does not alter execution semantics of the program.

Terminal indexing

Macro indexing enables accessing group terminals by their index. While each group terminal has its unique name, it is considered irrelevant by TML. Terminals are differentiated by their unique index. Accessing terminals that belong to a group via their name, instead of using macro indexing, is not allowed.

Following example shows macro indexing of terminals:

t.out{0}.type a = 0
t.out{1}.type b = 0

fn output_fnc():
    t.out{0} = 0
    t.out{1} = 0
end

Following example shows manually writen code equal to code after expansion:

# Terminal names were replaced by the names of
# concrete terminals with appropriate indices
t.out.type a = 0
t.out1.type b = 0

fn output_fnc():
    # This code is only metaphorically equivalent!
    # Users must always use macro indexing for terminal groups
    t.out = 0
    t.out1 = 0
end

User variable indexing

Macro indexing allows declaring and referencing (either in expressions or statements) of user variables declared either manually or via macro expansion. Index suffix is appended to the base name of the variable. Currently, no analysis is performed on the way of accessing macro expanded user variables, as long as the names do not clash inside a lexical scope.

Following example shows macro indexing of user variables:

a{0} = 0
int a{0, 1} = 0

fn test():
    a{0} = 0
    a{0, 1} = 0
end

Following example shows manually writen code equal to code after expansion:

# Variable names were prefixed with indices
a_0 = 0
int a_0_1 = 0

fn test():
    # Expanded code is equal to manually written code!
    # Users must take care of variable name clashing and shadowing
    # after macro expansion
    a_0 = 0
    a_0_1 = 0
end

macro for

macro for enables copying a portion of the code, parametrized with macro index variable from the loop header. In contrast to ordinary for, range expression must be statically evaluated, and unrolled code is generate. While contents of the statement block can be arbitrary statements, this is usually needed for iterating over group terminals. Macro block is discarded if the range for iteration is empty, or copied as many times as the range demands. Syntax analysis is performed for every macro for (in the initial parsing, before any macro expansion). Semantic analysis is performed for every instance of expanded macro for block. If the range is empty, no semantic analysis is performed. Every expanded instance has different value of macro index variable, according to the current macro expansion iteration.

If multiple iterations over same terminals are needed, multiple macro for blocks can be used through the code. Macro index variable can be arbitrarily used in all statements and expressions inside the macro for. Macro index variables use a simplified version of lexical scoping. Macro variable is visible only in the inner blocks, but is not allowed to have the same name as other nested macro or user variable.

Following example shows macro for:

macro for i=0:2:
    a{0} = 0 + i
end

fn test():
    macro for i=0:2:
        a{0} += i
    end
end

Following example shows manually writen code equal to code after expansion:

a_0 = 0 + 0
a_1 = 0 + 1

fn test():
    a_0 += 0
    a_1 += 1
end

macro if

macro if enables selecting a branch to be generated based on the condition. In contrast to ordinary if, expressions must be evaluated statically. Even if macro if has multiple conditions, expression evaluation is always performed to check whether all expressions can be evaluated statically, to prevent later combinatorial errors.

Either one or no branches are selected after expanding macro if. Syntax analysis is performed for all branches (in the initial parsing, before any macro expansion). Semantic analysis is performed only for the selected branch (if exists).

Following example shows macro if:

macro if n.flag1:
    a = 0
else:
    b = 0
end

fn test():
    macro if n.flag2:
        x = 0
    elseif n.flag3 or n.flag4:
        y = 0
    else:
        z = 0
    end
end

Following example shows manually writen code equal to code after expansion:

b = 0

fn test():
    y = 0
end