Lush/Macros
A macro is a meta-function that returns some code to be run. To the user of a macro, a macro looks and behaves exactly like a function. Indeed most users of Lush can become fairly fluent with its built-in libraries without concerning themselves with which functions are actually macros.
Why write a macro?
editMacros are more tedious to write than functions, and harder to debug. However, they offer advantages that make them worthwhile in some cases:
- Argument type flexibility in compiled code Often one wants to write a compilable function that can take any numeric type or idx as an argument. In compiled lush, you need to restrict each function to taking one specific type. Macro functions need not restrict themselves in that way, so long as the generated code compiles with the given argument types.
- Metaprogramming: Sometimes we end up writing many functions whose bodies are nearly identical except for a few isolated differences. A single macro can expand into these different functions depending on read-time traits of its arguments.
- Variable-length argument lists: Macros can have variable-length argument lists. This can be used as a compilable alternative to regular functions with
(optional&)
arguments, which are not compilable.
Limitations of compiled macros
editWhen compiling expressions that compile macros, the macro is deprived of much information that is only available at runtime. This limits compiled macros from using any kind of runtime information, such as:
- Argument type Type information, such as the numerical type of an idx, cannot be gleaned from the arguments of a macro at compile-time (macro arguments will typically have the generic symbol type
|SYMBOL|
). - Tensor dimensionality Unlike in some other programming languages, lush tensors of different dimensionality (e.g. vectors vs matrices) are all of the same type,
idx
. Thus having a macro make compile-time decisions based on idx dimension is a sure loser.
So what can you switch on in a compilable macro? Any value known at compile time, such as:
- Literals You can have a macro make compile-time decisions based on an
int
argument, as long as the argument is suppied as a literal, rather than the result of a computed expression. - Number of arguments See below for how to implement variable-length argument lists.
How to write a macro
editMacros are functions that are evaluated at read-time, and return function bodies. Consider this dirt-simple non-macro function:
; Function that adds two numbers
(de add-nums (num-1 num-2)
(+ num-1 num-2))
A macro equivalent could be written as follows:
; Macro that adds two numbers '''(naive implementation)'''
(dm add-nums-macro (fname num-1 num-2)
(list '+ num-1 num-2))
As you can see, macro functions are declared using dm rather than de. Also, they have an additional first argument, fname
, that holds the name of the macro. Finally, they don't calculate the answer of the function, they calculate an expression that, when evaluated, returns the function body. In the macro above, (list '+ num-1 num-2)
will evaluate out to (+ value_of_num-1 value_of_num-2)
at read-time.
The following will show you the basic changes you need to apply in order to mechanically transform a function into a macro. (Somebody should write a macro that does this!):
Replace lists with list-generators
editAny lush expression is a list of elements in parentheses:
(a b c d)
In the macro version of the function, all such lists should be changed into list-generators:
(list a b c d)
This rule should be applied recursively to each of the list elements. If the element is a list, replace it with a list-generator. Otherwise, quote it with a leading '
so that it gets returned as-is without being evaluated.
; normal code (sum (range 5)) ; macro code (list 'sum (list 'range 5))
Quote all non-argument symbols
editNotice in the example above, sum
and range
are preceded by an apostrophe. This prevents the interpreter from evaluating the value of the symbol "sum
" at read-time and changing it to "::DX:sum"
. Likewise, symbols such as operators ("+
"), class names ("gb-module
") and non-argument variables all need to be "quoted" with an apostrophe so they make it into the generated function body as-is.
Below are some examples of quoting:
; normal code: (+ 1 2 3) ; macro code (don't need to quote numeric literals): ('+ 1 2 3) ; normal code (embedded C code): #{ exit(0); #} ; macro code: '#{ exit(0); #} ; normal code (==> my-object some-method) ; macro code (list '==> 'my-object 'some-method) ; normal code (:my-object:some-slot) ; macro code (expands the ':' shorthand into the 'scope' function that it represents). (list 'scope 'my-object 'some-slot)
Note that literals such as the numeric literals 1
, 2
, and 3
above, or string literals such as "I'm a string"
, need not be quoted.
Put multi-line functions in an enclosing list
edit(Write about using list, progn, or let).
Use argument variables at most once
editThe add-nums-macro
example above is a naive implementation, because it uses the arguments directly. The problem with this is that every time num-1
or num-2
is used in the macro, is is evaluated. For example, if the user plugs in (incr x)
into the num-1
slot, that function will be called each time num-1
appears in the macro. This is fine in a simple macro such as add-nums-macro
, where each argument is used just once, but most functions refer to their arguments many times. An easy work-around is to assign the arguments to local variables at the beginning of the macro:
(dm idx-scale-and-add (idx-a-arg idx-b-arg)
(list 'let* (list (list 'idx-a idx-a-arg)
(list 'idx-b idx-b-arg))
;; scale idx-a by 5
(list 'idx-dotm0 'idx-a [d@ 5] 'idx-a)
;; add the scaled idx-a to idx-b
(list 'idx-add 'idx-a 'idx-b)))
Getting into the habit of enclosing your macro body with let*
has the additional benefit of making sure all your macro lines get returned, not just the last line (see previous section).
Debugging Macros
editOf the three kinds of code you can write in lush (interpreted, compiled, and macro), macro code is arguably the hardest to debug, since a bug could be in the read-time code generation or in the generated run-time code. When debugging the run-time code you can use the same functions used in debugging normal functions, only "macroized" as described earlier. In other words, (pause)
becomes (list 'pause)
and (printf)
becomes (list 'printf)
, etc. When debugging the read-time generation, you can use the following tools:
(macro-expand)
editThe first thing you should do after coding up your macro is to view the code it generates. Use (pretty (macro-expand <macro expression>))
, which formats and prints out the code generated by a macro expression.
Example:
;; Expanding the expression "(select (double-matrix 2 2) 0 1))"
? (pretty (macro-expand (select (double-matrix 2 2) 0 1)))
(let ((_-m (idx-clone (double-matrix 2 2))))
(idx-select _-m 0 1)
_-m )
(print)
editYou can also insert (print)
statements to print out the compile-time value of certain expressions, so long as you're careful that these (print)
statements don't change the value of the macro-expansion.