ADVANCED FANTASM FEATURES

The Power of Macros

Written by Rob Probin (May/June96)
(c) 1996 Lightsoft

Introduction

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.


BASIC MACROS

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.

MACRO PARAMETERS

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.

MACRO SIZES

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

CHECKING MACROS

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

EXTENDING FUNCTIONALITY

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.

TABLES AND THINGS

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.

OTHER INTERESTING NOTES

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.

CONTROL STRUCTURES WITH MACROS

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).

THE_DO_LOOP WITH EXIT_DOLOOP CONSTRUCTS

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.

THE FOR-NEXT LOOP

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.

THE REPEAT-UNTIL LOOP

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 IF_ELSE_THEN CONSTRUCT

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.

At Last - The End

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




Back to RANDOM ROB