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:
- TML source code is parsed (syntax analysis). Syntax errors are reported for both macro and non-macro code.
- Macro blocks are analyzed. Semantic errors related to macro blocks are reported.
- Macro blocks are expanded. Portions of code are either duplicated or discarded.
- 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