Saturday, August 6, 2022

HLL65F.MAC

HLL65F.MAC is a standard Atari macro library that adds some high-level language constructs to the 6502 assembler. It adds IF/THEN statements, conditional loops and a few other miscellaneous helper macros. Let's start by looking at an example of a conditional:

CPX #78
IFEQ
   ADC #2
ENDIF

First, keep in mind that this is not conditional assembly, there are pseudo ops for that, this is an actual runtime conditional. This macro check if the zero flag is set and if it is the code between the IFEQ and ENDIF is executed. Let's look at how these macros are expanded. First, a macro is used to define the IFEQ macro:

    DEFIF IFEQ,BNE

DEFIF is defined with this macro:

.MACRO DEFIF .1.,.2.

.NOCROSS .1.
  .MACRO .1.
IFXX .2.
.ENDM
.ENDM

It will expand to this:

.NOCROSS IFEQ
.MACRO IFEQ
    IFXX BNE
.ENDM

.NOCROSS tells the assembler not to add this symbol to the cross reference list. The rest of the expansion is the definition of a new macro which is just the IFXX macro with "bne" as the parameter. Here is the IFXX macro:

.MACRO IFXX .1.

LOC 0
.1. .
.ENDM

This expands to:


LOC 0
bne .

The first line is another macro which we will look at next. The second line branches back to itself. Since the macro is looking for Equals, this line will branch over the code in the block if the condition is Not Equals. Until the ENDIF is assembled the assembler doesn't know what the actual branch target should be, so the branch to self is used as a placeholder. Here is the LOC macro:

.MACRO LOC type
$INDEN
.PUSH REGSAV,...P0,...S0,...P1,...S1
...P0 = .
...S0 = type
.PUSH PC,...P0,...S0
.POP REGSAV,...S1,...P1,...S0,...P0
.ENDM

and here is the expansion:

$INDEN
.PUSH REGSAV,...P0,...S0,...P1,...S1
...P0 = .
...S0 = 0
.PUSH PC,...P0,...S0
.POP REGSAV,...S1,...P1,...S0,...P0

There are two possible values for the type parameter, 0 for IF blocks and 2 I believe is for a loop block but I haven't figured that one out yet. The INDEN macro is for doing indents in the listing and doesn't impact the code that is generated, I will code this macro in another post since it is a little mysterious. The second line saves a couple variables onto the REGSAV stack. ...P1 and ...S1 aren't  used so they didn't actually have to save then. Next it puts the current program counter into ...P0 and the type into ...S0 and pushes these on the PC stack, these will be used later to end the block. Finally the original variable values are restored from the stack.

Now that the block has been started we need to look at how it ends, in this case with the ENDIF macro:

.MACRO ENDIF
THEN
.ENDM

This macro is simply composed of another macro called THEN. I haven't dug into how you would use THEN by itself. THEN looks like this:

.MACRO THEN
FND
$UNDEN
.ENDM

just like $INDEN, $UNDEN is used for indentation in the list and I will cover this in another post, so that just leaves the FND macro:

.MACRO FND
.PUSH REGSAV,...P0,...S0,...P1,...S1
...P0 = .
.POP PC,...S1,...P1
.if eq,...s1&2
    . = ...P1+1
    .IF EQ,...S1&1
        ...S0 = ...P0-...P1-2
        .IIF GT,...S0-127.,.ERROR ...S0 ; BRANCH OUT OF RANGE
        .IIF LT,...S0+128.,.ERROR ...S0 ; BRANCH OUT OF RANGE
        .BYTE ...S0
    .IFF
        .WORD ...P0
    .ENDC
     . = ...P0
 .iff
     .ERROR TYPE ;inappropriate END for structure type
  .endc
 .POP REGSAV,...S1,...P1,...S0,...P0
.ENDM

This is the workhorse of this entire macro. It is basically going to use the information on the stack to modify the BNE opcode that was assembled in the IF macro so that it will branch past the block of code if the condition wasn't met. 

 First it saves that registers on the REGSAV stack so they can be restored at the end of the macro. Next the type is popped from the stack and stored in ...S1 and the program counter is popped and stored in ...P1. Next it checks if bit 1 or the type is set, in this case it will not be since the type is 0 so the condition will be met since the conditions is "equals zero". The next line will set the current program counter to the original program counter + 1 which will point it to the offset in the BNE opcode. Next bit 0 of the type is checked and again the condition is met. Next the offset for the branch is calculated by subtracting the original program counter from the program counter as it was when the macro started and then subtracting 2 and storing the result in ...S0. The next two lines check if the branch will be out of range, and if it is shows an assembler error.   After the just check is a .BYTE pseudo op that injects the new calculated offset into the BNE opcode that was assembled in the IF part of the macro. After the inner condition the current program counter is restored to where it was at the start of the macro, and then finally the registers are restored. 

These macros are a little complex but they do provide a clever way of implementing run time conditional blocks. The use of the assembler stack in the macros allows you to next IF blocks several levels deep just like in a higher level language. 



No comments:

Post a Comment