.define (also #define)

.define name[(argument [, argument [, ...]])] replacement

Defines a new (TASM-style) macro. Macros are basic find-and-replace operations, and as such lines of code are modified before being assembled. Because of this, you can do some silly things with macros; for example make XOR perform an AND operation, or call the accumulator fred.

The simplest macro will take one thing and replace it with another, such as:

.define size_of_byte 8
    ; ...
    ld a, size_of_byte

When the macro preprocessor sees the line ld a, size_of_byte it will get to work and replace it with ld a, 8.

It is important to realise that this is handled by the preprocessor - long before the actual code is even sent to the main assembler. As far as the actual assembler is concerned, that line never was never ld a, size_of_byte - it was always ld a, 8. The preprocessor only runs at the start of the first pass - this is why you cannot forward-reference macros. The reason for this is that one macro can affect another.

To give the macros a little more power, it is possible to define a macro that takes some arguments. The arguments are a comma-delimited list of argument names, and any instance of them in the replacement section of the macro will be substituted by the value passed when the macro is called. For example:

.define big_endian(value) .db value >> 8 \ .db value & $FF
    ; ...
    big_endian($F001)

This would assemble as .db $F001 >> 8 \ .db $F001 & $FF, displaying $F0, $01 in a hex editor, rather than the usual $01, $F0.

Multiple arguments aren't much more difficult:

.define call_on_success(test, success) call test \ or a \ call z, success
    ; ...
    call_on_success(open_file, read_file)
    ; ...

open_file:
    ld a, (hl)  ; (hl) contains 0 if file exists, 1 if it doesn't.
    ret

read_file:
    ; This will not get called if open_file fails (returns non-zero).
    ret

One special case macro is one where you don't give it any replacement and no arguments, such as .define FISHCAKES. In this case, the macro replaces itself with itself (so FISHCAKES becomes FISHCAKES), not nothing. However, a test to see if the macro exists through .ifdef FISHCAKES will still be true.

Another difference between TASM and Brass is that Brass has a more advanced macro system. A single macro name (such as call_on_success above) can have multiple replacements, and the correct one is identified by the replacement signature.

A replacement signature is the internal representation of the argument list in a macro. By default, each argument is treated as a wildcard, but by surrounding it with {} curly braces you can force it to be a particular string, for example:

.define my_macro(label) call label                              ; Signature of *
.define my_macro(label, variable) ld a,variable \ call label    ; Signature of *,*
.define my_macro(label, {a}) call label                         ; Signature of *,a
.define my_macro({0}, variable) call something \ ld a,variable  ; Signature of 0,*

The advantage of this is that you can create multiple macros - one being a general case macro, the others being specific cases where you can apply optimisations. Here's an example - let's say you had a function called sqr_root that you wanted to wrap in a macro for some reason. Here's the TASM approach:

#define sqrt(var) ld a,var\ call sqr_root

    sqrt(43)    ; Generates ld a,43\ call sqr_root
    sqrt(0)     ; Generates ld a,0\ call sqr_root (could be xor a!)
    sqrt(a)     ; Generates ld a,a\ call sqr_root (oh dear)

The Brass version would be:

.define sqrt(var) ld a,var\ call sqr_root
.define sqrt({0}) xor a\ call sqr_root
.define sqrt({a}) call sqr_root

    sqrt(43)    ; Generates ld a,43\ call sqr_root
    sqrt(0)     ; Generates xor a\ call sqr_root
    sqrt(a)     ; Generates call sqr_root

To make this sort of thing easier for yourself, it's a good idea to create a list of useful macros that handle the basic cases for you - for example, as a ld a,* replacement:

.define ld_a(var) ld a,var
.define ld_a({0}) xor a
.define ld_a({a})
; Now we have a sensible ld a,* macro replacement, use it to build the rest:
.define sqrt(var) ld_a(var)\ call sqr_root
.define cbrt(var) ld_a(var)\ call cube_root

Another possible use of this is to be able to assign defaults to arguments (Ion's sprite routine springs to mind - how often do you use it to display a non-8x8 sprite?)

; Assume ld_a()/ld_b() macros defined as above.
; _display is a function to display the number in 'a' in a number of bases:
; 'b' specifies which base we want to print it in.

.define display_in_base(var, base) ld_a(var)\ ld_b(var)\ call _display
.define display_in_base(var)       ld_a(var)\ ld b,10\ call _display

display_in_base(43,2)   ; Display 43 in base 2.
display_in_base(65,16)  ; Display 65 in base 16.
display_in_base(124)    ; Display 124 in base 10 (default).