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:
Name | Description |
---|---|
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 theisreg8()
andiscondition()
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.