Macros: debugging

Because of the complex structure you can implement with macros - defining constants, patterns, and language extensions -, debugging a program that uses macros presents some unique challenges. Here's how you can overcome some common problems that can occur when using macros.

Substitution problems

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 integer 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 integer 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-end
The parentheses ensure that the actions inside the macro body are executed first, meaning that the macro will execute consistently wherever it is used.

Pattern problems

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 => tag
This 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 | "-" | ".")* "/>" => tag
The 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

Multiple action problems

The same problem can occur with macros that encompass several actions. Careful, the code below is wrong:

  macro write token count * token text is
     local integer n
     repeat
        output text
        increment n
        exit when n > count
     again
  macro-end
  
  process
     local integer n
     set n to 5
     write 6 * "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 * token text is
     do
        local integer n initial {1}
        repeat
           output text
           increment n
           exit when n > count
        again
     done
  macro-end
  
  process
     local integer n initial {0}
     set n to 5
     write 6 * "fred%n"
     output "%d(n)%n"
  ; Output: " fred
  ;           fred
  ;           fred
  ;           fred
  ;           fred
  ;           fred
  ;           5"
  ; Note: The "*" character is simply a delimiter, not the multiplication operator.
  ;       It was chosen because it looks like the "*" or multiplication operator.
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.

Buffer problems

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

Watch those prepositions!

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 > 10
Is 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.

Parentheses problems

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, and the closing parenthesis after "rat" is not treated as a delimiter:

  ; string-containing.xom
  macro string-containing (arg open-d, arg target, arg close-d) is
     ((open-d) any** target any** (close-d))
  macro-end
  
  process
  submit "[we dropped freddy (the rat) off the fire escape] into the alley with military honors."
  find string-containing("[", ("escape"|"rat"), "]") => body
    output "String containing %"escape%" or %"rat%" = " || body || "END-OF-STRING%n"
  ; Output: "String containing "escape" or "rat" = [we dropped freddy (the rat) off the fire escape]END-OF-STRING
  ;           into the alley with military honors.

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 Topics