Written by Rob Probin (May/June96)
(c) 1996 Lightsoft
Welcome to the exciting world of Fantasm Macros!
I hope you'll enjoy this lightening tour of the power of macros. They really are a cool thing to use when writing assembler code, small or large.
But first a couple of notes...
I'm using 68K rather than PPC. This is not for any particular reason. Usually we have a 68K definition file and a PPC definition file. It's the concept that matters. I also assume some knowledge of 68K - I'm not going to explain any code, just present a series of examples showing you various concepts and ideas we've picked up.
Another point that I make is 'make your intentions known'. I've heard from myself so many times "I didn't expect me/anyone-else to look at this code again", when me/someone-else is trying to modify the code. By making it readable you allow your code and ideas to be reused and easily modified. 'A stitch in time saves nine' - a couple of seconds here means that several minutes in the future is saved. Check out old code at the detail level to see what I mean...can you understand it right away? Does it explain itself? The more understandable code is, the easier to spot bugs - they can't hide in the dark corners!
Let me point out a couple of macros are NEW to this document, and have not yet been fully tested out. Please email the Lightsoft support page if you have a problem. (lightsoft@zedworld.demon.co.uk)
Also, several macros need other macros, that are included elsewhere in this document.
There is one directive you should always know how to use whilst testing
macros: TRON I suggest to test a macro you write a small test program to
assemble in stand alone mode, and after trying it out, if it doesn't work,
put a TRON (thats short for TRace ON) at the top of the short test program
and watch what Fantasm is seeing.
You'll soon sort it out.
At the simplest form a macro is just a replacement tool. For example, anyone who wants to write RET instead of RTS could write:
ret MACRO rts ENDM
Which accomplishes the same task with an aliased name. Multiple instructions can of course be returned. For example, say all your subroutines will return an error in d0, with zero meaning no error, and -1 meaning error, you could use a pair of macros like these:
rts_no_error MACRO clr.l d0 ; using a long means all checks are ok rts ENDM rts_error MACRO moveq.l #-1,d0 rts ENDM
This would look something like this:
; check limit ; INPUT d0: Axis to check against overflow check_limit: cmp.l d0,#UPPER_LIMIT ; UPPER_LIMIT defined elsewhere (.def file?) blt error_condition rts_no_error error_condition: rts_error ; end of check_limit
Doing something simple like this means the intention is much easier to see.
Some other simple macros demonstrate the use of parameters. As parameters are just substitution within the macro, they are quite powerful. The lack of bz and bnz means that sometimes, when you are looking for zero explicitly, beq and bne don't explain what you are doing.
bnz: MACRO bne \1 ENDM bz: MACRO beq \1 ENDM
Simple, but useful.
tst.l d0 bnz quit_because_error
...expands to...
tst.l d0 bne quit_because_error
The first tells you explicitly what I'm looking for.
Also we can extend our return with error macros, to provide a flexible error number:
rts_error_code MACRO moveq.l #\1,d0 ; error codes -128 to 127 only! ENDM
Which will be used like:
rts_error_code -3
We could use a pregenerated error table to make the error numbers more meaningful. See 'TABLES AND THINGS' below.
You quite often need to be able to work with different sizes of data. For example, the 68K processors don't have an INC or DEC instruction.
inc MACRO addq.\0 #1,\1 ENDM dec MACRO subq.\0 #1,\1 ENDM
And they are used like
inc.l d0 dec.w d1
Push and Pop instructions are quite useful on other processors:
push MACRO move.\0 \1,-(sp) ENDM pop MACRO move.\0 (sp)+,\1 ENDM
I always include another for speed, when you just want a copy of the last (top) item. This could also be done using a pop followed directly by a push of the same name, hence the name.
poppush MACRO move.\0 (sp),\1 ENDM
Its always worth including some error checking within a macro. The two most common ways are with NARG (which is the number of arguments) and an IFC.
This probably contains too much checking, but demonstrates the principle.
inc MACRO ifne NARG-1 fail "NO REGISTER SPECIFIED!!!" endif ifc "","\0" fail "NO SIZE SPECIFIED!!!" endif add.\0 #1,\1 ENDM
Fantasm doesn't allow you to refer to labels directly in macros. This means, when you want to, for example, increment a label in a macro, it can be a pain. Macros themselves provide the solution.
INCSET MACRO \1 SET \1+1 ENDM
The opposite is DECSET...
DECSET MACRO \1 SET \1-1 ENDM
Which in a macro looks like this.
ANOTHER_MACRO MACRO ; ... more code ... INCSET current_count ENDM
One of the many areas which Lightsoft used to use macros to extend functionality is in the production of Pascal strings, which have a byte count at the beginning before the characters, unlike C strings which have a zero character at the end to signify the end.
There is a directive "PSTRING" as of version 4 that does this. However, for your information heres how you do it.
PASCAL_STRING MACRO dc.b end\@-start\@ start\@: dc.b \1 end\@: align ; protection with bytes! ENDM
The code:
PASCAL_STRING "Hello World!"
Generates.....
dc.b 12 dc.b "Hello World!" align
Notice the \@ in the macro. This produces a unique number each time the macros are used - so that the labels in other macros do not produce a 'label used twice' error.
A quite often occurring thing is an error table. You often find an 'error.def', included globally, providing the programs error numbering system.
This is typically something like:
NO_ERROR EQU 0 ERROR_SYNTAX_WRONG EQU -1 ERROR_PARSER_FAIL EQU -2 ERROR_ZERO_DIVIDE_IN_PERSPECTIVE EQU -3 ERROR_ZERO_DIVIDE_IN_ZERO_Z EQU -4 ERROR_ZERO_DIVIDE_IN_CLIPPING EQU -5 ERROR_ZERO_DIVIDE_IN_NORMALISER EQU -6 ERROR_LOW_MEMORY_IN_PARSER EQU -7 ERROR_LOW_MEMORY_IN_3D EQU -8
Now, if we want to add another zero divide error in, it becomes messy, especially with several hundred errors. Do you want to renumber all the errors? Do you put the new error at the end, out of place? Do you put a high number in the middle?
And what about if you want to associate a string with each error code? How do you keep them in sync??? And what about if you want a numeric table?
Macros are very helpful in this respect, you can reformat the data in one place, inside the macro.
An example is best:
; error.def
;
; Make ERROR_TABLE_FORMAT equal to...
; EQUATE_TABLE_ONLY for just the equates....
; STRING_TABLE_ONLY for the list of strings...
; LINKED_STRING_LIST for a linked list of strings...
CURRENT_ERROR_NUM SET 0
NEWERROR MACRO
ifeq ERROR_TABLE_FORMAT-EQUATE_TABLE_ONLY
\1 EQU CURRENT_ERROR_NUM
INCSET CURRENT_ERROR_NUM
else
ifeq ERROR_TABLE_FORMAT-STRING_TABLE_ONLY
PSTRING \2
else
ifeq ERROR_TABLE_FORMAT-LINKED_STRING_LIST
beginentry\@:
dc.l nextentry\@-beginentry\@ ; displacement to next entry
dc.l CURRENT_ERROR_NUM ; which number is this
dc.b \2,0 ; C style string
align
nextentry\@:
else
FAIL "Illegal Error Format"
endif
endif
endif
ENDM
NEWERROR NO_ERROR,"All OK"
NEWERROR ERROR_SYNTAX_WRONG,"Problem with Syntax"
NEWERROR ERROR_PARSER_FAIL,"Problem with parser"
NEWERROR ERROR_ZERO_DIVIDE_IN_PERSPECTIVE,"Divide by Zero Error in Pers"
NEWERROR ERROR_ZERO_DIVIDE_IN_ZERO_Z,"Divide by Zero Error in Z Clip"
NEWERROR ERROR_ZERO_DIVIDE_IN_CLIPPING,"Divide by Zero Error in Clipping"
NEWERROR ERROR_ZERO_DIVIDE_IN_NORMALISER,"Divide by Zero Error in Normaliser"
NEWERROR ERROR_LOW_MEMORY_IN_PARSER,"Low Memory Error in Parser"
NEWERROR ERROR_LOW_MEMORY_IN_3D,"Low Memory in 3D Routines"
; end of error.def
This file can then be included into several different files, the set up with ERROR_TABLE_FORMAT equal to several different values to produce different results.
REMEMBER! Macros don't take any time to call and return, unlike subroutines. A small sequence which would be nice to put into a subroutine for clarity but you think would slow the program down, can be put into a macro, which is easy to expand into a subroutine later if you need to.
(NOTICE: small sequences are quite often faster in subroutines than inline code on machines with caches!!! - never assume something, always test on several machines! - This is especially funny for C++ programmers, who have an explicit inline function type...)
Using MACROS, it is possible to build up a whole language, that the assembler compiles for you. Part of this could be the control structures you need - which is covered in the next section. Whatever your programming task, you can build up a specialist set of programming macros, which simplifies the implementation of the design.
In this section I take a look at a couple of common control structures. Using the principles included in these, it would be quite easy to implement things like SWITCH-CASE type statements, WHILE_WEND type statements, etc.
I've included an UNDERSCORE before each of these macros to ensure no problems occur with similar names in Fantasm's directives.
ANOTHER NOTE: I hate to admit this, but Fantasm v4 has a bug in it. Nested IF's don't work correctly. Since the REPEAT-UNTIL structure uses these as well as the IF_THEN_ELSE structure, you will need to upgrade to v4.04 before you can use it.
YET ANOTHER NOTE: Remember Fantasm's IF directive is the same as an IFNE (introduced for F4).
For our first macro is the simple DO-LOOP construct, that without extra code will loop forever. I've included an EXIT command as well. Let me explain why this makes this simple construct a more powerful tool.
The problem with some constructs is code duplication. Take this example in C:
i= array[index];
index++;
while(i != 30)
{
/* some action code here */
i=array[index];
index++;
}
Without making the code messy, its difficult to overcome this duplication (Before the loop and at the end of the loop). This duplication is bad because (a) it wastes space, (b) it is very bad for maintenance, since one section could be altered and not the other.
A better idea is an exit condition in the MIDDLE OF THE LOOP. ADA has one of these, and it comes in very handy. So here comes my version of the structure. NOTICE, the EXIT_DOLOOP statement is optional but obviously it will be infinite without it!!
move.b #0,d0 _DO counting_loop addq.b #1,d0 cmp.b #20,d0 bne miss_exit ; NOTE: ideal place for an _IF! (see below) _EXIT_DOLOOP counting_loop miss_exit: nop nop _LOOP counting_loop
And here's the code for the macros (this obviously needs to go at the top, before the above code in a test file, or in a specially included file).
_DO MACRO _doloop\1 ENDM _EXIT_DOLOOP MACRO bra _doloopexit\1 ENDM _LOOP MACRO bra _doloop\1 _doloopexit\1 ENDM
How does this work?
Well, the _DO provides a label for the top of the construct, and the _LOOP
macro provides the branch back to the top of the file. The _LOOP macro also
provides a label for the _EXIT_DOLOOP macro to go to when it want to exit.
The _EXIT_DOLOOP macro is simply a branch to this label.
The UNIQUE IDENTIFIER provided is used to tie together the _DO and _LOOP statements, as I couldn't see any easy way of doing this, and a small piece of text allows the programmer to specify the unique name for this loop anyhow.
The implementation of this construct, because of its simplicity, has one problem - the efficiency suffers. If you take a look at the example code, you'll see the branch before the _EXIT_DOLOOP - this means there are two branches produced - one with the condition and another to exit. The solution would be to make _EXIT_DOLOOP conditional. I've done this below for the _UNTIL construct and the _IF construct. However, I've avoided doing this for the _DO/_LOOP construct, as I feel its simplicity is much more important - in this case - than its efficiency.
I won't explain every nook and cranny of the following macros, merely give hints. If you wish to know exactly how they work, get your Fantasm Manual out, and pick through them instruction by instruction.
First lets take a look at a typical FOR-NEXT loop usage...
; ----- beginning of main ---------- NORMAL_VALUE EQU 3 NUMBER_OF_LOCATIONS EQU 10 main: debug lea area_to_default(pc),a1 _for clear_loop,d0,#1,#NUMBER_OF_LOCATIONS,#1 move.w #NORMAL_VALUE,(a1)+ _next clear_loop debug lea count(pc),a0 _for loser_loop,(a0),#1,#-10,#-2 nop nop nop _next loser_loop rts count: dc.l $123456 area_to_default: ds.w 10 ; ------ end of main ------------
The macro code to implement these "for" structures looks like this....
;
; FOR loop macro
; ==============
; Parameters:
;
; First: Unique FOR Identifier (Text label)
; Second: Register of count. Can be Dx, (Ax) or similar, but not x(pc).
; Third: Initial value. If numeric, then preceed with #.
; Fourth: Finish Value. If numeric, then preceed with #.
; Fifth: Step Value. If numeric, then preceed with #.
;
_FOR MACRO ; identifier,count reg, initial value, end value, step value
IFNE NARG-5
FAIL "if has wrong number of parameters"
ENDIF
move.l \3,\2
bra _forskip\1
_forloop1\1:
add.l \5,\2
_forskip\1:
cmp.l \4,\2
IFGE \5
bgt _forexit1\1 ; step positive: exit if register greater than end value
ELSE
blt _forexit1\1 ; step negative: exit if register less than end value
ENDIF
ENDM
_NEXT MACRO ; identifier
IFNE NARG-1
FAIL "Parameters wrong!"
ENDIF
bra _forloop1\1
_forexit1\1:
ENDM
; ----------END OF FOR LOOP MACRO--------
The first thing to notice is that all arithmetic is done using 32 bit numbers. This tends to mean the code generated is more general purpose.
I've also made the FOR macro do all the work - so that the only parameter in the NEXT macro is the loop name. This does mean an extra branch, but I think the simplicity of use is worth it.
Also notice the conditional assembly for the end condition check. I hadn't noticed this before, but the step size of the loop actually defines the check. See how I do not use equality. This is because with a weird step value it is quite possible to miss the end value.
This brings up exactly how to do expression evaluation, in the call or before it. Hence, I'm going to take the easier way out, and do it before. This it easier for me, but also gives much more power to the programmer. Also, I've included the functionality for multiple conditions to provide exit of the loop - these are executed in SEQUENCE, that is, the precedence is set by order of UNTIL statements.
Note: Don't confuse _REPEAT with Fantasm's REPEAT, and _UNTIL with Fantasm's UNTIL_cc!!!
Typical usage is as follows:
; THIS subroutine finds the 5 that is NOT followed by a 6 in an array, except ; ; assumption: the array ALWAYS zero terminated. lea array_start(pc),a0 _REPEAT find_the_5 move.b (a0)+,d0 ; now the until conditionals... cmp.b #0,d0 _UNTIL find_the_5,EQ,OR cmp.b #5,d0 _UNTIL find_the_5,EQ,AND cmp.b #6,(a0) _UNTIL find_the_5,NE rts array_start: dc.b 1,3,5,6,3,4,5,4,3,2,0
These are the macros...
_REPEAT MACRO ; identifier
IFNE NARG-1
FAIL "Parameters wrong!"
ENDIF
_repeatloop1\1:
ENDM
_RU_BRA MACRO ; identifier,test condition,0/1 normal/inverted
IFC "EQ","\2"
IFNE \3
bne _repeatloop1\1
ELSE
beq _repeatskip1\1
ENDIF
ELSE
IFC "NE","\2"
IFNE \3
beq _repeatloop1\1
ELSE
bne _repeatskip1\1
ENDIF
ELSE
IFC "LT","\2"
IFNE \3
bge _repeatloop1\1
ELSE
blt _repeatskip1\1
ENDIF
ELSE
IFC "GT","\2"
IFNE \3
ble _repeatloop1\1
ELSE
bgt _repeatskip1\1
ENDIF
ELSE
IFC "LE","\2"
IFNE \3
bgt _repeatloop1\1
ELSE
ble _repeatskip1\1
ENDIF
ELSE
IFC "GE","\2"
IFNE \3
blt _repeatloop1\1
ELSE
bge _repeatskip1\1
ENDIF
ELSE
FAIL "Unsupported Conditional Type"
ENDIF
ENDIF
ENDIF
ENDIF
ENDIF
ENDIF
ENDM
_UNTIL MACRO ; identifier,test condition,link condition(optional)
IFGT NARG-3
FAIL "Too many Parameters"
ENDIF
IFLT NARG-2
FAIL "Too few Parameters"
ENDIF
IFEQ NARG-2
; this is the conditional without any linkage, or the last in the line
_RU_BRA \1,\2,1
_repeatskip1\1:
ENDIF
IFEQ NARG-3
; determine whether OR or AND linkage
IFC "OR","\3"
_RU_BRA \1,\2,0
ELSE
IFC "AND","\3"
_RU_BRA \1,\2,1
ELSE
FAIL "Incorrect Linkage Parameter"
ENDIF
ENDIF
ENDIF
ENDM
As some general notes:
Look how I've included quite a bit of error checking. Error checking here is used everytime the macro is called, and could save the programmer (you or me) quite a bit of time tracking down a stupid mistake.
Secondly, I've included a 'sub-macro'. This simplifies the core macro code, making it less prone to errors, and shorter. Remeber - macros are just as prone to errors as standard code, so do anything to help this. Nested macros are a great tool - use them! The sub-macro in this case does all the conditional branch type material.
The classic IF_ELSE_THEN construct is produced here. This took a considerable less amount of time then the REPEAT_UNTIL construct, since I just stole the code and modified it from the REPEAT_UNTIL. Why waste time re-inventing the wheel?
; simple IF test program
; returns 0 if d0=$1234 or if d0=$1234 and d1 does not equal $1234,
; else returns -1.
cmp.l #$1234,d0
_IF checkout,EQ,OR
cmp.l #$1234,d1
_IF checkout,EQ,AND
cmp.l #$1234,d2
_IF checkout,NE
move.l #0,d0
_ELSE checkout
move.l #-1,d0
_ENDIF checkout
; These are the macros...
_IF_BRA MACRO ; identifier,test condition,0/1 normal/inverted
IFC "EQ","\2"
IFNE \3 ; =1 then branch to false if not condition
bne _iffalse_else\1
ELSE ; =0 then branch to true if condition
beq _iftrue\1
ENDIF
ELSE
IFC "NE","\2"
IFNE \3
beq _iffalse_else\1
ELSE
bne _iftrue\1
ENDIF
ELSE
IFC "LT","\2"
IFNE \3
bge _iffalse_else\1
ELSE
blt _iftrue\1
ENDIF
ELSE
IFC "GT","\2"
IFNE \3
ble _iffalse_else\1
ELSE
bgt _iftrue\1
ENDIF
ELSE
IFC "LE","\2"
IFNE \3
bgt _iffalse_else\1
ELSE
ble _iftrue\1
ENDIF
ELSE
IFC "GE","\2"
IFNE \3
blt _iffalse_else\1
ELSE
bge _iftrue\1
ENDIF
ELSE
FAIL "Unsupported Conditional Type"
ENDIF
ENDIF
ENDIF
ENDIF
ENDIF
ENDIF
ENDM
_IF MACRO
_if_else_is_missing\1 SET 1
IFGT NARG-3
FAIL "Too many Parameters"
ENDIF
IFLT NARG-2
FAIL "Too few Parameters"
ENDIF
IFEQ NARG-2
; this is the conditional without any linkage, or the last in the line
_IF_BRA \1,\2,1
_iftrue\1
ENDIF
IFEQ NARG-3
; determine whether OR or AND linkage
IFC "OR","\3"
_IF_BRA \1,\2,0
ELSE
IFC "AND","\3"
_IF_BRA \1,\2,1
ELSE
FAIL "Incorrect Linkage Parameter"
ENDIF
ENDIF
ENDIF
ENDM
_ELSE MACRO
bra _endifcond\1 ; after true code, get out
_iffalse_else\1
_if_else_is_missing\1 SET 0 ; else isn't missing
ENDM
_ENDIF MACRO
; this bit fills in if the 'else' part is missing...
IF _if_else_is_missing\1
_iffalse_else\1
ENDIF
; and the actual endif bit...
_endifcond\1
ENDM
Some basic notes:
In use the _ELSE part is, of course, optional.
If you understand how the _REPEAT/_UNTIL macros works then most of this is merely a variation.
I did consider using IFD and IFND, since the _ELSE part is optional, but we still need the label. However, IFD and IFND were not the way to do it - they are not invalidated between passes, so things defined after the conditional in pass 1, already exist in pass 2 - which effectively produce out-of-phase errors. The SET directive doesn't suffer with this since we can re-set it in the _IF part.
It has taken me several weeks to finish this short and fast tutorial - much longer than I thought it would have. It has turned out a couple of Fantasm problems, luckly in some ways, since we were having a general Fantasm/Eddie tidy up. The unlucky part about it is that it has taken time away from the compiler - but thats another story.
I hope you have had fun reading it, and maybe learnt a few new tricks - or at least it got you thinking a bit. I know, for all my harsh words, I had fun. Some of the problems in getting macros to work are like no others. Small satisfying routines, that in my case are ALWAYS useful. Trying to do the impossible, and succeeding in some way.
What are the limits on macros? The answer is a lot less than you think!!! Obviously, they have their limits: an expression evaluator on a single line is not a macro problem but a multi-line expression evaluator could be. The basic rule is - whatever makes the program easier to read, write and debug.
Have fun!
Rob Probin at Lightsoft