!lm12
!rm75
Jump Vectoring.............................Bob Sander-Cederlof

Applesoft has a statement which allows branching according to a computed index:

       ON X GO TO 100,200,300,400

Integer BASIC has a different method, simply allowing the line number after a GOTO, THEN, or GOSUB to be a computed value:

       GO TO X*100

Most other languages have some technique for vectoring to one of a series of places based on the value of a variable.  Modern languages like Pascal have a CASE statement, which can combine a comparison step.

       case PIECE of
          Pawn : ...;
          Knight : ...;
          Bishop : ...;
          Rook : ...;
          Queen : ...;
          King : ...;
       end

I frequently find myself building various schemes to handle the CASE statement in assembly language.  For example, I might accept a character from the keyboard and then compare it to a series of legal inputs, and branch accordingly to process the input.

One common way involves a series of CMP BEQ pairs, like this:

       JSR GETCHAR
       CMP #$81        control-A?
       BEQ ...            yes
       CMP #$84        control-D?
       BEQ ...            yes
       CMP #$8D        return?
       BEQ ...            yes
       et cetera

If there are not too many cases, and if the processing routines are not too far away for the BEQs to reach, this is a good way to do the job.  If the routines are bigger, and therefore tend to be too far away (causing RANGE ERRORS at assembly time), I might string together CMP BNE pairs instead:

       JSR GETCHAR
       CMP #$81        control-A?
       BNE TRY.D       no, try ctrl-D

      <code to process ctrl-A here>

TRY.D  CMP #$84        control-D?
       BNE TRY.M       no, try return

      <code for ctrl-D here>
TRY.M  CMP #$8D        return?
       BNE ... et cetera

      <code for ctrl-M here>

The trouble with the latter way is that programs get strung all over the place, and become very difficult to follow.  Unstructured, some would say.  The structure is really there, because we are just implementing a CASE statement; however, assembly language code over a sheet of paper long LOOKS unstructured, no matter what it is implementing.  And once a programmer gets his CASE statement spread over several sheets of paper, the temptation to begin making a "rat's nest" out of it can be overwhelming.

I prefer to put things into nice neat data tables.  Back in the August 1982 issue of AAL I presented a "Search and Perform" subroutine to handle a table like this:

       .DA #$81,CTRL.A-1
       .DA #$84,CTRL.D-1
       .DA #$8D,RETURN-1
       etc.

The table consists of three bytes per line, the first byte being the CASE value, and the other two being the address of the processing routine.

Another method is handy when the variable has a nice numeric range.  For example, what if I have processing routines for every possible control character from ctrl-A through ESC?  That is ASCII codes $81 through $9B.  If I subtract $81, I get a value from 0 through 26 (decimal).  If I then multiply the value by three, and add it to a base address, and store the result into another variable, and JMP indirect, I can access a series of JMPs to each processing routine:

       JSR GETCHAR
CASE   SEC
       SBC #$81
       CMP #27
       BCS ...ERROR, NOT IN RANGE
       STA ADDR        TIMES THREE
       ASL
       ADC ADDR
       ADC #TABLE      PLUS TABLE BASE ADDRESS
       STA ADDR
       LDA #0
       ADC /TABLE
       STA ADDR+1
       JMP (ADDR)
ADDR   .BS 2
TABLE  JMP CTRL.A
       JMP CTRL.B
       .
       .
       JMP ESCAPE

Note that if we got to the CASE program by doing a JSR CASE, then each processing routine can do an RTS to return to the main line program.  This makes our CASE look like it is doing a series of JSR's instead of JMP's.

We can shave bytes off the above technique by only keeping the address in TABLE, without all the JMP opcodes.  Then the variable only needs to be multiplied by two instead of three.  We will have to use the doubled variable for an index to pick up the address in the table and put it into ADDR:

       JSR GETCHAR
CASE   SEC
       SBC #$81
       CMP #27
       BCS ...ERROR, NOT IN RANGE
       ASL     DOUBLE THE INDEX
       TAX
       LDA TABLE,X
       STA ADDR
       LDA TABLE+1,X
       STA ADDR+1
       JMP (ADDR)
ADDR   .BS 2
TABLE  .DA CTRL.A
       .DA CTRL.B
       .
       .
       .DA ESCAPE

I don't recommend self-modifying code, but I still use it sometimes.  If you want to save two more bytes above, then you can store the jump address directly into the second and third bytes on a direct JMP instruction:

       LDA TABLE,X
       STA ADDR+1
       LDA TABLE+1,X
       STA ADDR+2
ADDR   JMP 0

A much better way involves pushing the processing routine address onto the stack, and using an RTS to branch to the pushed address.  Since RTS adds 1 to the address on the stack before branching, we have to push the address-1:

       LDA TABLE+1,X
       PHA             HIGH BYTE FIRST
       LDA TABLE,X
       PHA
       RTS
TABLE  .DA CTRL.A-1
       .DA CTRL.B-1
       .
       .
       .DA ESCAPE-1

Note that this method not only is not self-modifying, it also is a few bytes shorter and a tad faster.

All this is only necessary because the designers of the 6502 did not give us a JMP (addr,X) instruction.  If they had, we could do it like this:

       JSR GETCHAR
CASE   SEC
       SBC #$81
       CMP #27
       BCS ...ERROR
       ASL             DOUBLE FOR INDEX
       TAX
       JMP (TABLE,X)
TABLE  .DA CTRL.A, CTRL.B,...,ESCAPE

Then the hardware would add the doubled character offset (0,2,4,...52 for ctrl-A thru ESC) to the base address of the table, pick up the address from the table, and jump to the corresponding processing routine.

Since that would be so nice, and the designers agreed, the new 65C02 chip has it!  So if you know you are writing for a 65C02, and don't EVER intend to run in a plain 6502, you can use the JMP (TABLE,X).

It would also be nice to have JSR (TABLE,X), but you can simulate that by calling CASE with a JSR.  Or in other situations, you might merely do it this way:

       JSR CALL
       .
       .
CALL   JMP (TABLE,X)

Sometimes it so happens that your program can be arranged so that all the processing routines are in the same memory page.  Then there is no need to store the high byte of the address in the table, right?  Steve Wozniak thought this way, and you can see the result in the Apple monitor at $FFBE and following:

TOSUB  LDA #$FE        HIGH BYTE OF ALL ADDRESSES
       PHA
       LDA SUBTBL,Y
       PHA
ZMODE  LDY #0
       STY MODE
       RTS
       .
       .
SUBTBL .DA #BASCONT-1  CTRL-C
       .DA #USR-1      CTRL-Y
       .DA #BEGZ-1     CTRL-E
       .
       .
       .DA #BLANK-1    BLANK

Steve also used this technique inside the SWEET-16 interpreter.  You can see the code at $F69E through $F6C6 in the Integer BASIC ROM or RAM image.

If the routines are not necessarily all in one page, but are all within one 256-byte range, you can add an offset from the table to a known starting address.

Here is a method I would NEVER use, but it is cute, and short:

       LDA TABLE,X     X IS CALCULATED INDEX
       STA BRANCH+1    INTO BCC INSTRUCTION
       CLC             make branch always...
BRANCH BCC BRANCH      2ND BYTE GETS FILLED IN
BASE   .EQ *
       ...
       ...all the routines here
       ...
TABLE  .DA #CTRL.A-BASE
       .DA #CTRL.B-BASE
       etc.

The table has pre-computed relative offsets from BASE, so that the values can be plugged directly into the BCC instruction.  This is a fast and short technique, but somehow it scares me to think about self-modifying code.  If you need it, go ahead and use it!
