Klive Z80 Assembler
Macros

Macros

The Klive Assembler provides a powerful way to declare and apply macros in the code. While in most programming languages and assemblers, the macros are preprocessor constructs and use simple text replacement, the Klive implementation is different.

Note: Unlike in C, C++ (and some Z80 Assemblers), Klive macros emit only code (through instructions and pragmas); they cannot be used as user-defined functions. When you pass parameters to macros, any parameter expression is evaluated instantly, so you cannot use unknown symbols or variables, ones that will get their values only somewhere later in the code.

Getting Started with Macros

The best way to show you what macros can do is actual code. Let's start with a simple parameterless macro:

Delay: 
    .macro()    
    DelayLoop:
        djnz DelayLoop
    .endm

You can easily use this macro in your code:

ld b,#24
Delay()
; ...and later
ld b,#44
Delay()

The assembler will emit the code like this:

ld b,#24
DelayLoop_1: djnz DelayLoop_1
; ...and later
ld b,#44
DelayLoop_2: djnz DelayLoop_2

As you expect, it takes care that the DelayLoop label remains local within the scope of the macro; otherwise, it would lead to a duplicated label name.

This macro is named Delay, and it uses the value of the B register to create a djnz loop. You can easily apply this macro.

Now, let's enhance this macro with an argument:

Delay: 
    .macro(wait)    
        ld b,{{wait}}
    DelayLoop:
        djnz DelayLoop
    .endm

As the body of the macro suggests, {{wait}} is a placeholder for the wait argument. While other assemblers do not use a separate markup for a placeholder (they'd just use wait), Klive applies this markup for two reasons: first, it is visually better and more eye-catching; second, it allows the compiler to provide better performance.

You can use this macro passing an argument value for wait:

Delay(#24)
Delay(d)

As you expect, the compiler now emits this code:

ld b,#24
DelayLoop_1: djnz DelayLoop_1
; ...and later
ld b,d
DelayLoop_2: djnz DelayLoop_2

Macros allow you to pass anything that could be an operand in a Z80 instruction, so this is entirely valid:

Delay((ix+23))

As you can imagine, this macro invocation results as if you wrote this:

ld b,(ix+23)
DelayLoop: djnz DelayLoop

Klive macros do not stop here. You can define macros that receive an entire Z80 instruction as an argument:

RepeatLight: 
    .macro(count, body)    
        ld b,{{count}}
    DelayLoop:
        {{body}}
        djnz DelayLoop
    .endm

This macro is to repeat the body in count several times. This is how you can invoke it:

RepeatLight(4, "add a,c")

The second argument of the macro is a string that names the add a,c operation. The result of this macro is this set of instructions:

ld b,4
DelayLoop_1: 
  add a,c
djnz DelayLoop_1

Instead of a run time loop, you can apply a compile time loop within the macro:

RepeatLight: 
    .macro(count, body)
      .loop {{count}}
          {{body}}
      .endl
    .endm

The RepeatLight(3, "add a,c") line invokes the macro, and the macro's body translates to this:

.loop 3
    add a,c
.endl

As you already learned, the compiler handles this as if you wrote:

add a,c
add a,c
add a,c

Klive allows you to pass a set of lines as a macro argument. You can invoke RepeatLight like this:

RepeatLigth(3, "add a,c" & "add a,10")

Or, you can make it with variables:

FirstOp = "add a,c"
SecondOp = "add a,10"
RepeatLight(3, FirstOp & SecondOp)

Note: The & operator between the two string values concatenates them with a next-line character set (#0A and #0D). If you apply the + operator, the above code would fail: the assembler accepts only a single instruction in a text line and would reject multiple instructions.

In macros, you can use several unique functions, such as lreg() and hreg(). These work during parse time and retrieve the lower and higher 8-bit register of a 16-bit register pair:

LdHl: 
    .macro(reg16)
        ld h,hreg({{reg16}})
        ld l,lreg({{reg16}})
    .endm

Here, you can apply the LdHl macro like this:

LdHl(de)
LdHl(bc)

The compiler translates these macro invocations into these Z80 instructions:

ld h,d
ld l,e
ld h,b
ld l,c

When you invoke a macro, you can pass fewer parameters than the macro declares. Within the macro body, you can use the def() function to check whether the specified parameter has been passed:

Push:
    .macro(r1, r2, r3, r4)
      .if def({{r1}})
        push {{r1}}
      .endif
      .if def({{r2}})
        push {{r2}}
      .endif
      .if def({{r3}})
        push {{r3}}
      .endif
      .if def({{r4}})
        push {{r4}}
      .endif
    .endm

The Push macro in this code snippet allows you to create a push instruction for up to 4 register pairs. Look at these usages:

Push(af)
; ... and later
Push(bc, de, ix)

This is what the compiler generates:

push af
; ... and later
push bc
push de
push ix

You can opt not to pass a macro parameter for a specific argument. Look at this macro declaration:

LdBcDeHl:
    .macro(bcVal, deVal, hlVal)
      .if def({{bcVal}})
        ld bc,{{bcVal}}
      .endif
      .if def({{deVal}})
        ld de,{{deVal}}
      .endif
      .if def({{hlVal}})
        ld hl,{{hlVal}}
      .endif
    .endm

You can invoke this macro in these ways, leaving a parameter empty to sign that you do not intend to use it:

LdBcDeHl(,#1000,#2000)
; ... and later
LdBcDeHl(#3000,,#4000)

The compiler understands your intention and generates this output:

ld de,#1000
ld hl,#2000
; ... and later
ld bc,#3000
ld hl,#4000

It's time to go deep into the nitty-gritty details of creating and using macros in Klive.

Macro Declaration

Macros must have a name. Each macro is named according to the label preceding its declaration, either in the same line as the .macro token or before it as a hanging label. Macros can have zero, one, or more named arguments separated by a comma. The macro declaration is closed with the .endm token:

MacroWithHangingLabel:
    .macro(myParam, otherParam)
    ; Macro body
    .endm

MyMacro: macro()
    ; Macro body
    .endm

Even if a macro does not have arguments, its declaration must contain the parentheses.

The macro body can contain Z80 instructions, pragmas, or statements. The only exceptions are the ENT and XENT pragmas.

As you already experienced, the Assembler supports syntax variants for the macro-related keywords. The Assembler accepts these tokens: .macro, macro, .MACRO, MACRO, .endm, endm, .ENDM, ENDM, .mend, mend, .MEND, and MEND.

Within the macro's body, you can refer to the arguments of the macros wrapping them into double curly braces:

Mul10:
    .macro(reg8)
    push af
    ld a,{{reg8}}
    add a,a
    push bc
    ld b,a
    add a,a
    add a,a
    add a,b
    pop bc
    ld {{reg8}},a
    .endm

Arguments are identifiers. Thus, the corresponding naming rules are applied to them. You cannot use a reserved word (for example, a mnemonic like ldir or a register name like hl) as a macro argument.

Macro Parameters

You can invoke a macro with as many parameters as many arguments its declaration has or even with fewer parameters. If the macro invocation has more parameters than arguments, the compiler raises an error.

Let's assume you've created this macro declaration:

MyMacro: .macro(arg1, arg2, arg2)
; Macro body
.endm

All of these usages are valid:

MyMacro()
MyMacro(a)
MyMacro(a, b)
MyMacro(a, b, c)

Nonetheless, this usage is invalid since it passes more than three parameters:

MyMacro(a, b, c, d) ; ERROR: To many parameters

Sometimes, it is convenient to omit not the last parameters but one at the beginning or the middle of the parameter list. You can do that: an empty comma separator signs that the preceding parameter is empty. Using this notation, all these invocations of MyMacro are valid:

MyMacro(,b)
MyMacro(a,,c)
MyMacro(,,)

Within the macro declaration, you can use the def() function to check if a particular argument has a value.

LdBcDeHl:
    .macro(bcVal, deVal, hlVal)
      .if def({{bcVal}})
        ld bc,{{bcVal}}
      .endif
      .if def({{deVal}})
        ld de,{{deVal}}
      .endif
      .if def({{hlVal}})
        ld hl,{{hlVal}}
      .endif
    .endm

The def() function accepts only a macro argument reference (the argument's name wrapped in double curly braces). This function evaluates to true only when the macro argument is not empty.

You can use the logical NOT operator (!) combined with def() to check if an argument is empty.

MyMacro: .macro(arg)
  .if !def({{arg}})
    ; generate something for empty arg
  .endif
.endm

Passing Parameters to Macros

You can pass anything as a macro parameter that is a valid operand of a Z80 instruction:

  • Names of 8-bit registers and 16-bit register pairs (e.g. a, b, ixl, hl sp, af, etc.)
  • Names of conditions (e.g. z, nz, pe, m, etc.)
  • Memory address indirection (e.g. (#4000), (#4000+#20))
  • Register pair indirection (e.g. (bc), (de), (hl), etc.)
  • Indexed indirection (e.g. (ix+#20), (iy-12), etc.)
  • C-port ((c))
  • Expression (e.g. (MyId << 1) + 23, #4000, 12*sin(pi()/4), "ld " + "a,b", etc.)

You should be careful when you use parentheses in expressions. Let's assume you declare this macro:

SetHlValue:
    .macro(value)
        ld hl,{{value}}
    .endm

When you use it, the first invocation uses an expression, and the second has a memory address indirection:

SetHlValue(#4000+#20)
SetHlValue((#4000+#20))

The compiler translates them to these instructions:

ld hl,#4020
ld hl,(#4020)

To avoid such issues, you can use the square brackets to group parts of expressions. When you invoke the SetHlValue macro with this way, both usage with generate a ld hl,#4020 statement:

SetHlValue(#4000+#20)
SetHlValue([#4000+#20])

Note: When you pass parameters to macros, any parameter expression is evaluated instantly, so you cannot use unknown symbols or variables (ones that will get their values only somewhere later in the code.)

The compiler replaces the macro argument references to their current values passed in parameters. Whenever you use an expression, its value is converted into a string and put into the place of the macro argument.

Passing Instructions in a Macro Parameter

Within a macro declaration, you can use a macro argument reference instead of an entire Z80 instruction. Take a look at this macro:

ShortDi:
    .macro(body)
        di
        {{body}}
        ei
    .endm

Here, the body argument is expected to get something the compiler can understand as an entire instruction. When you invoke the macro, you need to pass a string expression so that the compiler can replace the {{body}} reference. Here is an example:

ShortDi("in a,(#fe)")

As you expect, the compiler generates this output:

di
in a,(#fe)
ei

You are not obliged to use Z80 instructions; the compiler accepts pragmas, too:

ShortDi(".db #00")

Well, the output is not pretty useful; nonetheless, the compiler generates this:

di
.db #00
ei

Passing Multiple Lines in a Macro Parameter

If you pass multiple lines in a macro parameter where the corresponding argument reference is used instead of an entire instruction line, the compiler will apply all those lines. In this case, the individual lines should be separated by new line characters (\r\n). The & operator, when applied for two strings, does this step for you, as it concatenates the two strings with \r\n between them. Let's assume you invoke the ShortDi macro with this code:

ShortDi("ld a,#7f" & "in a,(#fe)")

Now, the compiler will generate this output:

di
ld a,#7f
in a,(#fe)
ei

Because you can pass expressions as macro parameters, you can invoke the macro in this way, too:

FirstOp = "ld a,#7f"
SecondOp = "in a,(#fe)"
ShortDi(FirstOp & SecondOp)

You can pass not only instructions and pragmas to macros but also statements:

LoopOp = ".loop 3" & "nop" & ".endl"
ShortDi(LoopOp)

The compiler will emit this code:

di
nop
nop
nop
ei

Labels, Symbols, and Variables in Macros

Macros have a local scope for all labels, symbols, and variables created within their body, including the label attached to the .endm statement. The macro's name is a label that also represents the start of the macro.

Take a look at this macro definition:

GetBoundaries:
    .macro(instr)
        {{instr}}
        ld de,EndLabel
        ld hl,GetBoundaries
EndLabel:
    .endm

Here, the ld hl,GetBoundaries instruction fills HL with the start address of the macro, while the ld de,EndLabel instruction puts the address of the next instruction following the macro into DE.

Let's assume you use the macro this way:

.org #8000
GetBoundaries("nop")
GetBoundaries("ld ix,#ABCD")

The compiler will create this output:

#8000 GetBoundaries_1 nop
#8001                 ld de,#8007 ; EndLabel_1
#8004                 ld hl,#8000 ; GetBoundaries_1
#8007 EndLabel_1
      GetBoundaries_2 ld ix,#ABCD
#800B                 ld de,#8011 ; Endlabel_2
#800E                 ld hl,#8007 ; GetBoundaries_2
#8011 EndLabel_2

Symbols and variables within the context work exactly as they do with loops. Remember: Symbols are constant values, while variables may change!

Invoking Macros from Macros

Klive allows you to invoke a macro from another macro, too. Here is a short sample:

Delay:
    .macro(wait)
        ld b,{{wait}}
        WaitLoop: djnz WaitLoop
    .endm

BorderPulse:
    .macro(col1, wait1, col2, wait2)
        ld a,{{col1}}
        out (#fe),a
        Delay({{wait1}})
        ld a,{{col2}}
        out (#fe),a
        Delay({{wait2}})
    .endm

Here, the BorderPulse macro uses Delay as a helper macro. The BorderPulse(2, 10, 3, 20) invocation produces this output:

ld a,2
out (#fe),a
ld b,10
WaitLoop_1: djnz WaitLoop_1
ld a,3
out (#fe),a
ld b,20
WaitLoop_2: djnz WaitLoop_2

Macro-Related Parse-Time Functions

The Klive Assembler allows several parse-time functions with macro arguments similar to the def() function to check whether a macro argument has been passed to the macro invocation.

These functions check if the argument is an operand the name of the function suggests. Each of them returns true, provided the function recognizes the operand; otherwise, false.

The Assembler supports these functions:

NameDescription
isreg8std()The operand is an 8-bit register, one of these: a, b, c, d, e, h, l, i, r, xh (ixh), xl (ixl), yh (iyh), or yl (iyl)
isreg8std()The operand is a standard 8-bit register, one of these: a, b, c, d, e, h, or l
isreg8spec()The operand is a special 8-bit register, i, or r
isreg8idx()One of these 8-bit index registers: xh (ixh), xl (ixl), yh (iyh), or yl (iyl)
isreg16()Any of these 16-bit registers: af, bc, de, hl, sp, ix or iy
isreg16std()Any of the standard 16-bit registers: bc, de, hl, or sp
isreg16idx()Any of the ix or iy registers
isregindirect()The operand is one of these: (bc), (de), (hl), or (sp)
isindexedaddr()The operand is an indexed address like (ix), (iy), (ix+#12), (iy-#23), and so on
iscport()The operand is (c) (e.g., in the out (c),a instruction)
iscondition()The operand is one of these conditions: z, nz, c, nc, po, pe, p, or m
isexpr()The operand is an expression, for example: 1 + 2, #1000, myvalue + 23, etc.
isreg<reg>()The operand is the register as given in <reg>. You can use these names: a, af, b, c, bc, d, e, de, h, l, hl, i, r, xh, xl, ix, yh, yl, iy, and sp. For example, isrega() tests if the specified register is A.

Note: When you pass 'c' as a macro argument, both the isreg8() and iscondition() parse-time functions accept it, as the 'c' token can be either an 8-bit register or a condition (carry flag is set).

Here is a short sample:

MyRegMacro: .macro(arg)
    .if isreg8({{arg}})
        ld a,{{arg}}
    .else
        .error "Only 8-bit registers are allowed"
    .endif
.endm

MyRegMacro allows using only an 8-bit register as its argument. If you provide another type of parameter, the macro raises an error.