|
|||||
Writing robust macros | |||||
Related Syntax |
OmniMark's macro facility is extremely powerful. It allows you to define constants, patterns, and language extensions to suit your needs. Macros work by substituting macro text at the place where the macro is invoked; if macros are carelessly defined this can lead to strange behavior. Consider the following macro which fails to take into consideration order of operations:
macro sum (arg a, arg b) is a+b macro-end process local counter n set n to 5 * sum(2,4) output "%d(n)"
It looks as though this program should output "30", which is 5 multiplied by the sum of 2 and 4. In fact it will output "14". Why? Because the "sum" macro expands so that the program actually reads:
process local counter n set n to 5 * 2 + 4 output "%d(n)"
To avoid this, the macro should be written with parentheses around the macro body:
macro sum (arg a, arg b) is (a+b) macro-endThe parentheses ensure that the actions inside the macro body are executed first, meaning that the macro will execute consistently wherever it is used.
The same principle applies to macros that express patterns. Remember that any element of a find rule's pattern may be written to a pattern variable. Consider what would happen when a pattern variable is used with the following macro:
macro empty-tag is "<" letter (letter | number | "-" | ".")* "/>" macro-end find empty-tag => tagThis program looks as though it ought to find empty XML tags (without attributes) and place them in the pattern variable "tag". What it really does is match empty tags and place "/>" in the pattern variable because the macro expands so that the find rule reads:
find "<" letter (letter | number | "-" | ".")* "/>" => tagThe cure is the same -- use parentheses to ensure that the defined pattern is treated as a unit:
macro empty-tag is ("<" letter (letter | number | "-" | ".")* "/>") macro-end
The same problem can occur with macros that encompass several actions:
macro write token count times token text is local counter n repeat output text increment n exit when n > count again macro-end process local counter n set n to 5 write 6 times "fred%n" output "%d(n)%n"This program looks as though it ought to write out "fred" six times and then output "5". In fact it outputs an error message because, when the macro is expanded, there is a duplicate definition of the local variable "n", which is not allowed.
The answer in this case is to wrap the actions in a macro in a do...done block:
macro write token count times token text is do local counter n repeat output text increment n exit when n > count again done macro-end process local counter n set n to 5 write 6 times "fred%n" output "%d(n)%n"This code behaves as expected. A new variable scope is created by the do...done block, which protects the variable declaration in the macro and preserves the value of the variable "n" in the surrounding code. Written like this, your macro will work regardless of any variable declarations in the code in which it is used.
You should also take care that your macros do not have unforeseen consequences. The following macro is used to append text to a buffer. The macro is careful to check whether the buffer is open or closed and to leave it the way it found it:
macro append to buffer token the-buffer token the-string is do local switch initially-open initial {false} set initially-open to (the-buffer is open) do when the-buffer isnt attached open the-buffer as buffer done do when the-buffer isnt open reopen the-buffer done put the-buffer the-string do unless initially-open close the-buffer done done macro-end process local stream fred append to buffer fred "Mary had a little lamb%n" output fred process local stream fred open fred as buffer append to buffer fred "Mary had a little lamb%n" close fred output fred
By the way, this macro is written to make the point very clear, but there is a much more succinct way of doing the same thing:
macro append to buffer token the-buffer token the-string is do when the-buffer is open put the-buffer the-string else set the-buffer the-string done macro-end
When you use macros to create language extensions, you should expect them to be used in various combinations, without the person using them necessarily having to think about whether they are using a macro or part of the language. When designing macros, therefore, make sure they integrate smoothly with each other and with the rest of the language. For instance, these two macros create ambiguity:
macro size arg thing of token other-thing is number of attribute thing of other-thing macro-end macro of grandparent is of parent of parent macro-end element #implied do when size widths of grandparent of parent > 10Is the "of" following "widths" in the line above the "of" that is part of the macro name "grandparent of" or the "of" delimiter of the first
arg
parameter of the "size" macro? Faced with this question, OmniMark always chooses to recognize the ambiguous term as a delimiter rather than as a macro name, but you should not write macros that rely on this behavior. A language extension should be clear and unambiguous to the user, not just the compiler.
Note that parentheses (of all types) receive special treatment in this regard. If a closing parenthesis is used as a delimiter, proper nesting of parentheses is respected in expanding the macro. Thus, the following works as you would expect:
macro upto (arg pat) is ((lookahead not (pat)) any)+ macro-end find ul "begin" upto (ul ("end" | "finish")) => body
The closing parentheses of ("end" | "finish")
are not treated as the delimiter. In all other cases, the first occurrence of the delimiter would be treated as the delimiter.
If you are ever unsure about how your macros are being expanded, run your program with the "-expand" command-line option. This causes OmniMark to output a version of your program with all macro expansions complete.
Related Syntax %@ literal macro |
---- |