Apple Assembly Line
Volume 4 -- Issue 11 August 1984

In This Issue...

New Cross Assemblers Available

We have recently added three new cross assemblers to the S-C Macro family.

     Intel Z-8...................$32.50
     General Instruments 1650....$50.00
     General Instruments 1670....$50.00

Unlike previous cross assemblers, which were based on Version 1.0 of the S-C Macro Assembler, these are based on Version 1.1. This means 80-column support for the Videx, STB-80, and Apple //e-//c 80-column, as well as standard 40-column. It also adds certain directives and fixes some problems which were in version 1.0.

We have also been hard at work generating Version 2.0 of the S-C Macro Assembler. It will be ready soon, complete with a brand new manual. It will support all the new opcodes and address modes of the 65C02, 65802, and 65816 processors. Owners of older versions of the S-C Assemblers will be able to upgrade for a very reasonable fee.

18-Digit Arithmetic, Part 4 Bob Sander-Cederlof

This month we will look at two output conversion routines. The first one always prints in exponential form, while the second one allows setting a field width and number of fractional digits. The routines are written so that the output string may either be printed or fed to an Applesoft string variable.

Let's assume that the value to be printed has already been loaded into the DP18 accumulator, DAC. Lines 1230-1270 describe DAC as a 12-byte variable. The exponent is in the first byte, DAC.EXPONENT. It has a value from $00 to $7F:

       $00 means the whole number is zero
       $01 means the power of ten exponent is -63
       $3F means 10^-1
       $40 means 10^0
       $41 means 10^1
       $7F means 10^63

The 18 digits of the number, plus two extension digits, are in the next ten bytes in decimal format (each digit takes four bits). The extension is zeroed when you load a fresh value into DAC, but after some computations it holds two more digits to guard against roundoff and truncation problems.

The sign of the number is stored in DAC.SIGN: if the value in DAC.SIGN is from $00 to $7F, the number is positive; if from $80 to $FF, the number is negative.

If you have been following the DP18 series from the beginning, and typing in all the code (or getting it from the Quarterly Disks), then you will realize that to integrate each installment takes some work. In order to print the sections separately, and have them separately readable, I must repeat some variable declarations. The listing this month refers to two previously printed subroutines, DADD and MOVE.YA.ARG. These are simply equated to $FFFF in lines 1030 and 1040, so that the code will assemble. If you really want it to work, you have to remove those two lines and include the code for the subroutines. The fact that three installments have already been printed also somewhat restricts me, because even if I see possible improvements I must be careful not to contradict the code you already have.

Quick Standard Format Conversion

The subroutine QUICK.FOUT which begins on line 1600 converts the contents of DAC to a string in FOUT.BUF in the format


The first s is the sign, which is included only if negative. The d's are a series of up to 18 significant digits (trailing zeroes will not be included). The letter E is always included, to signify the power-of-ten exponent field. The letter s after the E is the sign of the exponent: it is always included, and will be either + or -. The xx is a two-digit exponent, and both digits will always be included. The decimal point will be included only if there are non-zero digits after it. If the number is exactly zero, the string in FOUT.BUF will be simply "0". Here are some more examples:

       value           string
       1000            "1E+03"
       .001            "1E-03"
       -262564478.5    "-2.625644785E+08"

Two processes are involved in converting from DAC to FOUT.BUF. One is the analysis of the DAC contents; the other is the process of storing sequential characters into FOUT.BUF. The latter process is handled in most cases by the subroutine at lines 3720-3820. Entry at STORE.CHAR stores the contents of the A-register in the next position in FOUT.BUF, and increments the position pointer (INDEX). Entry at STORE.DIGIT first converts the value in the A-register to an ASCII digit by setting the high nybble to "3". (The digits 0-9 are $30-$39 in ASCII.)

QUICK.FOUT begins by setting INDEX, the FOUT.BUF position pointer, to 0. At lines 1630-1700 the special case of the value in DAC being exactly zero is tested and handled. If the value in DAC is zero, then DAC.EXPONENT will be zero. (This is a convention throughout DP18, to simplify making values of zero and testing for them.) If the value is zero, ASCII zero is stored in FOUT.BUF, followed by a terminating $00 byte.

If the value is not zero, the next job is to check the sign of the value. Lines 1710-1740 insert a minus sign in FOUT.BUF if the value is negative.

Lines 1760-1910 pull out the 18 digits of the mantissa from DAC.HI through DAC.HI+8. The extension digits are ignored. The code here looks an awfully lot like a routine to convert from hex to ASCII, ignoring the possible hex digits A-F. That is because the digits are four bits each, and ARE like hex digits. Lines 1830-1860 insert the decimal point after the first digit.

Lines 1930-2020 look at the formatted number in FOUT.BUF and trim off the trailing zeroes. If all digits after the decimal point are zero, the decimal point is trimmed off too. If you would rather that QUICK.FOUT always printed exactly 18 digits, trailing zeroes and all, you could cut out these lines.

Lines 2040-2290 format the exponent field. First the letter E is installed in FOUT.BUF. Then lines 2060-2120 install the exponent sign. There is a little adjustment here due to the fact that the value in DAC is in the form ".DDDD" times a power of ten, and we are converting to "D.DDD" times a power. That means the exponent in DAC.EXPONENT is one larger than we will print. The DEY at line 2080 adjusts for this offset.

Lines 2130-2180 get the absolute value of the exponent by removing the $40 bias and taking the 2's complement if the result is negative. Lines 2190-2290 convert the binary value of the exponent to two decimal digits, and insert them into FOUT.BUF. Lines 2300-2310 terminate the FOUT.BUF string with $00.

Once the value has been converted to a string in FOUT.BUF, we can either print it or put it into an Applesoft string variable. The subroutine QUICK.PRINT which begins at line 1370 calls QUICK.FOUT and then prints the characters from FOUT.BUF.

Fancier Formatted Conversion

The second conversion routine, which begins at line 2350, allows you to specify the number of digits to display after the decimal point, and the number of characters in the output field. The value will be formatted into the field against the right end, with leading blanks as necessary to fill the field. The value will be rounded to the number of digits that will be converted. If you are familiar with the FORTRAN language, you will recognize this as the "Fw.d" format. W is the width of the field, and D is the number of fractional digits. Here are some examples:

     W  D   value   string
     12 5   1234.56 "  1234.56000"
     12 1   1234.56 "      1234.6"

As before, the output string will be stored in FOUT.BUF in ASCII code, terminated by a $00 byte. If the value will not fit into the W.D field you specify, the entire field will be filled with "*" characters.

As listed here, I have set FOUT.BUF as a 41-byte variable. This means the maximum W is 40, leaving room for the terminating $00 byte. If you want longer conversions, simply change line 1060.

FOUT expects the W and D parameters to be in the A- and Y-registers, respectively. Lines 2380-2460 check W and D for legality. If W is larger than FOUT.BUF.SIZE-1, then it is set to that value. We don't want to store converted characters beyond the end of FOUT.BUF! Then if D is larger than W-1, it is pruned back.

Lines 2480-2540 initialize various variables used during the following conversion. Once again, INDEX will point to the position in FOUT.BUF. I could probably have economized some in the use of variables by re-using the same variables for different purposes, but I wanted to keep them separate to make it easier to code and debug.

Line 2560 calls ROUND.DAC.D to round the value in DAC to D digits after the decimal point. This boils down to adding .5 times 10 to the D power to the value in DAC. ROUND.DAC.D, at lines 3860-4000, does just that. First the rounding number is built in ARG, then DADD adds ARG to FAC.

Lines 2570-2610 store a minus sign into SIGN.CHAR if the value in DAC is negative. SIGN.CHAR was initialized to $00 above. If the sign is negative, line 2590 will increment SIGN.SIZE. SIGN.SIZE will either be 0 or 1, and will be used later in determining how many leading blanks are needed. We cannot store the sign character into FOUT.BUF until the leading blanks have been stored.

Lines 2630 to 2710 compute how many digits will be printed before the decimal point (NO.LEADING.DIGITS), and how many zeroes before the first significant digit after the decimal point (NO.LEADING.ZEROES). If the power-of-ten exponent was negative, there will be no leading digits and some leading zeroes; if positive, there will be some leading digits and no leading zeroes. For example,

    .2345E-5     .000002345    5 leading zeroes
    .2345E+3     234.5         3 leading digits

What if the exponent is more than 18? This would mean more digits might be extracted from DAC than exist, so lines 2730-2790 limit NO.LEADING.DIGITS to 18. NO.INTEGRAL.ZEROES takes up the slack, to print any necessary zeroes between the last significant digit before the decimal point, and the decimal point. For example, if W=25 and D=2, and the value is .1234E+20, we will get NO.LEADING.DIGITS=18 and NO.INTEGRAL.ZEROES=2:

       "  12340000000000000000.00"

Lines 2810-2870 now calculate the total number of non-blank characters which will be required: one for sign if the sign is negative, all the leading digits and integral zeroes before the decimal point, one for the decimal point itself, and D fractional digits. (Just now I noticed that I could have saved two bytes and two cycles by changing line 2810 from CLC to SEC, and eliminating the ADC #1 at line 2860.)

Lines 2890-2920 compute how many significant digits of fraction will be needed. You specified D digits of fraction, but only DD of them will come from the value in DAC. This will be less than D if there are any leading zeroes.

Lines 2940-2970 check whether the converted number can fit in a W-wide field. If not, Lines 3370-3400 fill the field with stars and exit.

Lines 2980-3030 compute how many leading blanks will be needed to right justify the number in the W-field. There is some hopscotch here because we are going to put "0." in front of numbers that have no integral digits.

At long last, we are ready to begin string characters in FOUT.BUF. Lines 3050-3070 store the leading blanks. A subroutine STORE.N.CHARS does the dirty work. STORE.N.CHARS (lines 3670-3710) expects the character to be stored in the A-register, and the count in the Y-register. It also expects that the Z-status is set according to the count in Y. Thus, if the count is zero, the subroutines returns immediately without storing any characters.

STORE.N.DIGITS, at lines 3440-3620, is quite similar to STORE.N.CHARS. Once again, the count must be in the Y-register and the Z-status should reflect the value in Y. Digits are picked out of the value in DAC using an index DIGIT.PICKER, and stored into FOUT.BUF using STORE.DIGIT.

Lines 3090-3110 store the sign if it is negative. Lines 3120-3210 print whatever digits are needed before the decimal point. This will include leading digits (if any) and integral zeroes (if any), or simply one zero (if neither of the other).

Lines 3230-3320 store the fractional part. This includes the decimal point, leading fractional zeroes (if any), and fractional digits (if any).

Finally, lines 3340-3350 store a terminating $00 at the end of the string in FOUT.BUF.

A subroutine called FORMAT.PRINT at line 1450 calls FOUT and then prints the contents of FOUT.BUF. You could now write a higher level routine, if you wish, which would examine the exponent to determine whether the number would fit in a 20-character field. If not, you could use QUICK.PRINT. If so, use FOUT with W=40 and D=18, and then truncate leading spaces and trailing zeroes. This would give you a complete print routine for any numbers, printing them in simple form when they fit and exponential form when they don't. Indeed, just such a routine already exists in DP18, but will have to wait for a future installment. FOUT can also be used as the base for a complete PRINT USING facility, and that is also already in DP18 waiting for future installments.

Meanwhile, enjoy these two conversions, and experiment with your own.

  1000 *SAVE S.DP18 FOUT
  1010 *--------------------------------
  1020 AS.COUT       .EQ $DB5C
  1030 DADD          .EQ $FFFF
  1040 MOVE.YA.ARG   .EQ $FFFF
  1050 *--------------------------------
  1060 FOUT.BUF      .BS 41
  1080 *--------------------------------
  1090 W          .BS 1
  1100 D          .BS 1
  1110 INDEX      .BS 1
  1120 SIGN.SIZE  .BS 1
  1130 SIGN.CHAR  .BS 1
  1140 ZERO.CHAR  .BS 1
  1150 WW         .BS 1
  1160 DD         .BS 1
  1170 DIGIT.PICKER        .BS 1
  1220 *--------------------------------
  1230 DAC           .BS 12
  1250 DAC.HI        .EQ DAC+1
  1270 DAC.SIGN      .EQ DAC+11
  1280 *--------------------------------
  1290 ARG           .BS 12
  1310 ARG.HI        .EQ ARG+1
  1330 ARG.SIGN      .EQ ARG+11
  1340 *--------------------------------
  1350 *   QUICK PRINT
  1360 *--------------------------------
  1380        JSR QUICK.FOUT
  1390        JMP FOR.PRINT.1
  1400 *--------------------------------
  1420 *      (A)=WIDTH OF FIELD
  1430 *      (Y)=# OF FRACTIONAL DIGITS
  1440 *--------------------------------
  1460        LDX #'0      USE ZEROES BEFORE FRACTION
  1470        STX ZERO.CHAR
  1480        JSR FOUT
  1490 *--------------------------------
  1500 FOR.PRINT.1
  1510        LDY #0
  1520 .1     LDA FOUT.BUF,Y
  1530        BEQ .2
  1540        JSR AS.COUT
  1550        INY
  1560        BNE .1       ...ALWAYS
  1570 .2     RTS
  1580 *--------------------------------
  1590 *      QUICK CONVERSION
  1600 *--------------------------------
  1620        LDY #0
  1630        STY INDEX
  1640        LDA DAC.EXPONENT
  1650        BNE .0       NUMBER IS NOT ZERO
  1660        INC INDEX
  1670        STY FOUT.BUF+1
  1680        LDA #'0
  1690        STA FOUT.BUF  MAKE IT '0'
  1700        RTS
  1710 .0     LDA DAC.SIGN
  1720        BPL .1
  1730        LDA #'-      NEGATIVE
  1740        JSR STORE.CHAR
  1750 *--------------------------------
  1760 .1     LDA DAC.HI,Y NEXT BYTE OF #
  1770        PHA
  1780        LSR
  1790        LSR
  1800        LSR
  1810        LSR
  1820        JSR STORE.DIGIT
  1830        CPY #0
  1840        BNE .2
  1850        LDA #'.      PUT DECIMAL POINT
  1860        JSR STORE.CHAR
  1870 .2     PLA          DO 2ND DIGIT
  1880        JSR STORE.DIGIT
  1890        INY
  1900        CPY #9       8 MORE BYTES
  1910        BCC .1
  1920 *--------------------------------
  1940 .3     DEY
  1950        LDA FOUT.BUF,Y
  1960        CMP #'0
  1970        BEQ .3       DONE
  1980        CMP #'.      TRAILING DECIMAL PT?
  1990        BNE .4       NO
  2000        DEY          YES, DELETE IT
  2010 .4     INY
  2030 *--------------------------------
  2040        LDA #'E
  2060        LDA #'+
  2070        LDY DAC.EXPONENT
  2080        DEY
  2090        CPY #$40
  2100        BCS .5
  2110        LDA #'-
  2120 .5     JSR STORE.CHAR
  2130        TYA          EXPONENT
  2140        SEC
  2150        SBC #$40     REMOVE OFFSET
  2160        BPL .6
  2170        EOR #$FF
  2180        ADC #1
  2190 .6     LDX #'0-1
  2200        SEC
  2210 .8     INX
  2220        SBC #10
  2230        BCS .8
  2240        ADC #'9+1
  2250        PHA
  2260        TXA
  2270        JSR STORE.CHAR
  2280        PLA
  2290        JSR STORE.CHAR
  2300        LDA #0
  2310        JMP STORE.CHAR
  2320 *--------------------------------
  2340 *      (A)=WIDTH OF FIELD
  2350 *      (Y)=# OF FRACTIONAL DIGITS
  2360 *--------------------------------
  2370 FOUT
  2390        BCC .1
  2400        LDA #FOUT.BUF.SIZE-1
  2410 .1     STA W
  2420        CPY W        FORCE D<W
  2430        BCC .2
  2440        TAY
  2450        DEY
  2460 .2     STY D
  2470 *--------------------------------
  2480        LDA #0
  2490        STA INDEX
  2500        STA SIGN.SIZE
  2510        STA SIGN.CHAR
  2540        STA DIGIT.PICKER
  2550 *--------------------------------
  2570        LDA DAC.SIGN
  2580        BPL .3
  2590        INC SIGN.SIZE
  2600        LDA #'-      MINUS SIGN
  2610        STA SIGN.CHAR
  2620 *--------------------------------
  2630 .3     SEC
  2640        LDA DAC.EXPONENT
  2650        SBC #$40     REMOVE OFFSET
  2660        BPL .4
  2670        EOR #$FF
  2700        LDA #0
  2720 *--------------------------------
  2730        SEC
  2750        SBC #18
  2760        BMI .5
  2780        LDA #18      18 SIGNIF DIGITS MAX
  2800 *--------------------------------
  2810 .5     CLC          CALCULATE TOTAL # OF DIGITS
  2820        LDA SIGN.SIZE
  2850        ADC D
  2860        ADC #1
  2870        STA WW
  2880 *--------------------------------
  2890        SEC
  2900        LDA D
  2920        STA DD
  2930 *--------------------------------
  2940        SEC
  2950        LDA W
  2960        SBC WW
  2970        BMI .14      ...OVERFLOW
  3000        BNE .6
  3020        BPL .6
  3040 *---STORE LEADING BLANKS---------
  3050 .6     LDA #' '     BLANK
  3070        JSR STORE.N.CHARS
  3080 *---STORE SIGN-------------------
  3090        LDA SIGN.CHAR
  3100        BEQ .8
  3110        JSR STORE.CHAR
  3120 *---STORE INTEGRAL DIGITS--------
  3140        BEQ .10
  3150        JSR STORE.N.DIGITS
  3160        BEQ .11      ...ALWAYS
  3180        JSR STORE.CHAR
  3190 .11    LDA #'0
  3210        JSR STORE.N.CHARS
  3220 *---STORE FRACTION---------------
  3230        LDA #'.
  3240        JSR STORE.CHAR
  3250        LDA DD
  3270        BEQ .13
  3280        LDA ZERO.CHAR
  3300        JSR STORE.N.CHARS
  3310        LDY DD
  3320        JSR STORE.N.DIGITS
  3330 *---TERMINATE STRING-------------
  3340 .13    LDA #0
  3350        JMP STORE.CHAR
  3360 *--------------------------------
  3370 .14    LDA #'*'     FILL FIELD WITH STARS
  3380        LDY W
  3390        JSR STORE.N.CHARS
  3400        JMP .13
  3410 *--------------------------------
  3420 *   STORE NEXT (Y) DIGITS
  3430 *--------------------------------
  3450        CMP #20
  3460        BCC .1
  3470        LDA #0
  3480        BEQ .2       ...ALWAYS
  3490 .1     LSR          LEFT/RIGHT --> C
  3500        TAX          INDEX --> X
  3510        INC DIGIT.PICKER
  3520        LDA DAC.HI,X
  3530        BCS .2
  3540        LSR
  3550        LSR
  3560        LSR
  3570        LSR
  3580 .2     JSR STORE.DIGIT
  3590        DEY
  3610        BNE SND..1
  3620        RTS
  3630 *--------------------------------
  3650 *      (Z-STATUS IF COUNT IS 0)
  3660 *--------------------------------
  3680        DEY
  3700        BNE SNC..1
  3710        RTS
  3720 *--------------------------------
  3740 *--------------------------------
  3760        AND #$0F
  3770        ORA #'0'
  3790        LDX INDEX
  3800        STA FOUT.BUF,X
  3810        INC INDEX
  3820        RTS
  3830 *--------------------------------
  3850 *--------------------------------
  3860 ROUND.DAC.D
  3870        LDA DAC.SIGN  GET THE SIGN
  3880        PHA          SAVE IT
  3890        LDA #CON.1HALF
  3900        LDY /CON.1HALF
  3910        JSR MOVE.YA.ARG   MOVE .5*10^-D INTO ARG
  3920        PLA          GET SIGN
  3930        STA ARG.SIGN
  3940        LDA D        GET # OF PLACES
  3960        SEC          ADD 1 DURING NEXT ADD
  3970        ADC #$40     ADD OFFSET
  3980        STA ARG.EXPONENT
  3990        JMP DADD     ADD .5*10^-D;FOUT WILL TRUNCATE IT
  4000 *--------------------------------
  4010 CON.1HALF .HS 4050000000000000000000

Enable/Disable IRQ from Applesoft Bob Sander-Cederlof

If you have applied the patches to DOS 3.3 that we published in the January 1984 issue (pages 10,11), and if you now are using interrupts from such sources as the Timemaster II or a handy pushbutton, you have probably run into the need to enable and disable IRQ from within an Applesoft program. (That sentence is the kind you have to read without interruption, so I really should have begun the paragraph with SEI and ended it with CLI.)

What is need is four bytes of assembly language, at a location that you can CALL. For example:

       300- 58    CLI
       301- 60    RTS
       302- 78    SEI
       303- 60    RTS

If those four bytes are in memory as shown, you can CALL 768 to enable IRQ interrupts, and CALL 771 to disable them. You can install the four bytes like this:

       100 POKE 768,88: POKE 769,96
       110 POKE 770,120:POKE 771,96

Now there are often times when poking into page 3 is not possible. Are there other tricky ways to get those bytes installed, without using page 3?

I found a half dozen or so. First, realize that the four bytes only need to be there when you call them. The rest of the time the same locations could be used for other purposes. For example, we could poke them into the input buffer at $200, as long as we do it every time we CALL it:

       100 POKE 512,88:POKE 513,96:CALL 512
             to enable interrupts, or

       500 POKE 512,120:POKE 513,96:CALL 512
             to disable them.

The result of a multiplication or division is left, sometimes normalized and sometimes not, in $62...$66. If we find two numbers whose product leaves the bytes $58 and $60 at $62 and $63, we could CALL 98:

       100 X = 1*707 : CALL 98 : REM ENABLE IRQ
       200 X = 1*963 : CALL 98 : REM DISABLE IRQ

On the next page is a table showing the various methods I found:

      Enable (CLI..RTS)      Disable (SEI..RTS)
       100 POKE 38,88            100 POKE 38,120
       110 POKE 39,96            110 POKE 39,96
       120 CALL 38               120 CALL 38
       100 CALL 8411232-8411065  100 CALL 8419424-8419257
       100 GOSUB 24664           100 GOSUB 24696
       ...                       ...
       24664 CALL 117:RETURN     24696 CALL 117:RETURN
       100 X = 1*707 : CALL 98   100 X = 1*963 : CALL 98
       100 X = RND(-8411323.5)   100 X = RND(-8419424.5)
       110 CALL 203              110 CALL 203
       110 NORMAL:CALL1024       110 NORMAL:CALL1024

Can you figure out how all these work? They are pretty tricky! Can you think of some more?

Line Number Cross Reference Bill Morgan

Have you ever had to modify a BASIC program written by someone who didn't seem to know what he was doing? Deciphering several hundred undocumented lines of split FOR/NEXTs and tangled GOTOs can lead to a severe headache. We recently had a consulting job that involved just such a project: one program to be altered was about a hundred sectors of spaghetti-plate Applesoft. A couple of the biggest problems were figuring out which lines used a particular variable, and what lines called others, or were called from where.

Back in November of 1980, AAL published a Variable Cross Reference program which neatly took care of the first problem by producing a listing in alphabetical order of all the variables used and all the lines using them. At the end of that article, Bob S-C pointed out that the program could, with some effort, be modified into just the sort of Line Number Cross Reference we now needed. Well, I drew the job of making that modification, and here's what I came up with.

The Basis

These Cross Reference programs use a hash-chain data structure to store the called and calling line numbers. Each called line has its own list of lines which refer to it. We locate these lists by using the upper six bits of the line number for an index into a table located at $280. This table contains the address of the beginning of each of the 64 possible chains. Each chain is made up of the data for a range of 1024 possible called line numbers. The first one has called lines 0-1023, the second has 1024-2047, and so on.

The entry for each called line is made up of a pointer to the next called line in that chain, this called line number, a pointer to the next calling line, and the number of this calling line. Each subsequent calling line entry has only the last four bytes. A pointer with a value of zero marks the end of each chain and each list.

VCR used three characters for each variable: the first two letters of the variable name and a type designator of "$", "%" or " ". The first character was the hash index and the last two characters were stored at the beginning of each variable's chain. LCR uses the high-order 6 bits of the called line number for the hash index and stores both bytes of the number in the chain. This is slightly redundant, so if you want to store more information about the called line, you can use the upper six bits of the chain entry.

VCR stored the calling line numbers with the high byte first, backwards from usual 6502 practice. This was done so the same search-compare code could handle both variable names and line numbers. To simplify the conversion I kept the same structure, even though it's no longer strictly necessary.

The Program

LCR, the overall control level, is identical to VCR and just calls the other routines.

INITIALIZATION prepares a couple of pointers and zeroes the hash table. The only difference here is the size of the hash table.

PROCESS.LINE is also the same as in VCR. This routine steps through the lines of the Applesoft program, moving the calling line number into our data area and JSRing to SCAN.FOR.CALLS to work on each line.

SCAN.FOR.CALLS is the first really new section of code. We start by setting a flag used to mark ON ... GO statements. Then we step through the bytes of the line, looking for tokens that call another line. GOTO and GOSUB are processed immediately. For a THEN token we check to see if the next character is a number. If it is, we deal with it; if not, we go on. If we find an ON token, we set the flag and keep looking. After a GOTO or GOSUB we check ONFLAG. If there was an ON, we look for a comma to mark another called line number.

PROCESS.CALL first converts the ASCII line number of the called line into a two-byte binary number and then searches the data structure for that line. If it is there, we simply add this calling line to the list. If we don't find the called line we create a new entry for it.

CONVERT.LINE.NUMBER is lifted straight from Applesoft's LINGET, at $DA0C.

NEXT.CHAR is a utility routine to get the next byte from the program and advance the pointer.

SEARCH.CALL.TABLE starts the search pointer on the appropriate chain.

CHAIN.SEARCH uses the pointer in an entry to step to the next entry. If the pointer is zero, then there is no next entry and the search has failed. We then compare the line number in the entry to the one we're looking for. If the entry is less than the search key, we go on. If it is equal, we update the pointer and report success. If we hit an entry greater than the key, the search fails and we return.

SEARCH.LINE.CHAIN is called after SEARCH.CALL.TABLE has found a match. Here we move the pointer to the calling line field of the matching entry and use the current calling line for a search key.

ADD.NEW.ENTRY first updates the pointers in the previous entry and this new entry, and the end-of-table pointer. We then make sure there is room for the new entry and move the data up into the new space.

Now we are done with the routines devoted to building the Cross Reference tables. Interestingly, SEARCH.CALL.TABLE, CHAIN.SEARCH, SEARCH.LINE.CHAIN, and ADD.NEW.ENTRY are the real heart of this program, and the only change I had to make in these routines from VCR to LCR was to alter the method of figuring the hash index in SEARCH.CALL.TABLE. Next we come to getting the data back out of the tables and onto a display.

PRINT.REPORT first sets a pointer we'll be using later on and then steps through the hash table, calling PRINT.CHAIN for each entry found.

PRINT.CHAIN starts out by checking for a pause or abort signal from the keyboard. It then moves the current called line number into LINNUM, checks to see if it really exists, and prints it, followed by an asterisk if it is undefined. Now we move a pointer up to the start of the calling line list and call PRINT.LINNUM.CHAIN to display all the entries. The last step is to move the pointer up to the next called line in this chain, if any, and go back to do that one.

CHECK.DEFINITION keeps its own pointer into the program and steps along checking each called line to see if it actually exists. It provides a space or an asterisk to be printed after the line number.

PRINT.LINNUM.CHAIN displays the calling lines stored for each called line. We first tab to the next column (or line if necessary), then get the line number out of the list and print it. Lastly, we move the pointer up to the next entry, if any, and loop back.

TAB.NEXT.COLUMN prints enough blanks to move over to the next output position. If a new line is necessary, it checks the line number to see if the new line should go to the screen only, or also to a printer. This is Louis Pitz's addition, designed to automatically handle either 40- or 80-column output.

PRINT.LINE.NUMBER and CHECK.FOR.PAUSE are pretty standard routines to convert a two-byte binary number into five decimal characters, and to provide for pause/abort during display.

Well, now we have a Line Number Cross Reference to go along with the Variable Cross Reference. Now all that remains is to integrate the two programs into one master Applesoft Cross Reference Utility. Maybe you could call it with "&V" for VCR, or "&L" for LCR, and simply "&" to get both listings. Any takers out there?

PS: Bob suggested that I add a diagram of the hash chain structure, and a summary of the search process. OK, here they are... diagram of hash chain

One of
64 hash                  Calling Line Lists

  ----      ---------      ----
 |    |--->|    |    |--->|    |--->0
  ----      ---------      ----

            ---------      ----      ----      ----
           |    |    |--->|    |--->|    |--->|    |--->0
            ---------      ----      ----      ----
Chain       ---------
           |    |    |--->0

            ---------      ----
           |    |    |--->|    |--->0
            ---------      ----

Found a Call

    * Use high 6 bits of called line number to index Hash Table
    * Get pointer from Hash Table to find start of chain
         * If no pointer in Hash Table, make new entry
    * Search chain for same line number
         * If not found, make new link in chain
         * If found, search calling line list
    * Enter new calling line in list
  1000 *SAVE S.LCR
  1010 *--------------------------------
  1040 *
  1050 *       Based on Variable Cross Reference
  1060 *       Original by Bob S-C 11/80
  1070 *       Modified by Louis Pitz 8/83
  1080 *       Adapted by Bill Morgan 8/84
  1090 *--------------------------------
  1100        .OR $6000
  1110 *      .TF B.LCR
  1120 *--------------------------------
  1130        LDA #$4C     set & vector
  1140        STA $3F5
  1150        LDA #LCR
  1160        STA $3F6
  1170        LDA /LCR
  1180        STA $3F7
  1190        RTS
  1200 *--------------------------------
  1210 TEMP    .EQ $15
  1220 COUNTER .EQ $16
  1230 ONFLAG  .EQ $17           ON ... GO flag
  1240 DEFFLAG .EQ $17
  1250 PNTR    .EQ $18,19        pointer into program
  1260 LZFLAG  .EQ $1A           leading zero flag
  1270 DATA    .EQ $1A thru $1D
  1280 NEXTLN  .EQ $1A,1B        address of next line
  1290 LINNUM  .EQ $1C,1D        current line number
  1300 STPNTR  .EQ $1E,1F        pointer into call table
  1310 TPTR    .EQ $9B,9C        temp pointer
  1320 ENTRY   .EQ $9D thru $A4  8 bytes
  1330 CALL    .EQ ENTRY+2
  1340 SIZE    .EQ $A5,A6
  1350 HSHTBL  .EQ $280
  1360 *--------------------------------
  1370 PRGBOT  .EQ $67,68        beginning of program
  1380 LOMEM   .EQ $69,6A        beginning of variable space
  1390 EOT     .EQ $6B,6C        end of variable table
  1400 *--------------------------------
  1410 COMMA      .EQ ',
  1420 CR         .EQ $8D
  1430 TKN.GOTO   .EQ $AB
  1440 TKN.GOSUB  .EQ $B0
  1450 TKN.ON     .EQ $B4
  1460 TKN.THEN   .EQ $C4
  1470 *--------------------------------
  1480 MON.CH     .EQ $24
  1490 KEYBOARD   .EQ $C000
  1500 STROBE     .EQ $C010
  1510 AS.MEMFULL .EQ $D410
  1520 MON.PRBL2  .EQ $F94A
  1530 MON.CROUT  .EQ $FD8E
  1540 MON.COUT   .EQ $FDED
  1550 MON.COUT1  .EQ $FDF0
  1560 *--------------------------------
  1580 .1     JSR PROCESS.LINE
  1590        BNE .1            until end of program
  1600        JSR PRINT.REPORT
  1610        JSR INITIALIZATION  erase call table
  1620        LDA #0            clear $A4 so Applesoft
  1630        STA $A4           will work correctly
  1640        RTS
  1650 *--------------------------------
  1670        LDA LOMEM         start call table
  1680        STA EOT           after program
  1690        LDA LOMEM+1
  1700        STA EOT+1
  1710        LDX #$80          # of bytes for hash pointers
  1720        LDA #0
  1730 .1     STA HSHTBL-1,X
  1740        DEX
  1750        BNE .1
  1760        LDA PRGBOT        start pointer at
  1770        STA PNTR          beginning of program
  1780        LDA PRGBOT+1
  1790        STA PNTR+1
  1800        RTS
  1810 *--------------------------------
  1830        LDY #3            capture pointer and line #
  1840 .1     LDA (PNTR),Y
  1850        STA DATA,Y
  1860        DEY
  1870        BPL .1
  1880        LDA DATA+1        check if end
  1890        BEQ .3            yes, return .EQ.
  1900        CLC
  1910        LDA PNTR          adjust pointer to
  1920        ADC #4            skip over data
  1930        STA PNTR
  1940        BCC .2
  1950        INC PNTR+1
  1960 .2     JSR SCAN.FOR.CALLS
  1970        LDA DATA          point to next line
  1980        STA PNTR
  1990        LDA DATA+1        and return .NE.
  2000        STA PNTR+1
  2010 .3     RTS
  2020 *--------------------------------
  2040        LDA #$FF
  2050        STA ONFLAG
  2060 .1     JSR NEXT.CHAR
  2070        BEQ .4
  2080        CMP #TKN.THEN     scan for call token
  2090        BEQ .2
  2100        CMP #TKN.GOTO
  2110        BEQ .3
  2120        CMP #TKN.GOSUB
  2130        BEQ .3
  2140        CMP #TKN.ON
  2150        BNE .1            no match, keep going
  2160        LSR ONFLAG        set flag for ON token
  2170        BPL .1         ...always
  2190 .2     LDY #0            after THEN, check
  2200        LDA (PNTR),Y      for line number
  2210        CMP #'0
  2220        BCC .1            <0 isn't
  2230        CMP #'9+1
  2240        BCS .1            neither is >9
  2260 .3     JSR PROCESS.CALL  handle this call
  2270        LDA ONFLAG        are we in ON?
  2280        BMI SCAN.FOR.CALLS no, go on
  2290        JSR NEXT.CHAR     yes, look for comma
  2300        BEQ .4            EOL
  2310        CMP #COMMA
  2320        BEQ .3            comma, get another call
  2330        BNE SCAN.FOR.CALLS ...always
  2350 .4     RTS
  2360 *--------------------------------
  2400        BCC .2            found same call
  2410        LDA #0
  2420        STA ENTRY+4       start of line number chain
  2430        STA ENTRY+5
  2440        LDA LINNUM+1      MSB first
  2450        STA ENTRY+6
  2460        LDA LINNUM
  2470        STA ENTRY+7
  2480        LDA #8            add 8 byte entry
  2490 .1     JMP ADD.NEW.ENTRY
  2520        BCC .3            found same line number
  2530        LDA #4            add 4 byte entry
  2540        BNE .1         ...always
  2560 .3     RTS
  2570 *--------------------------------
  2590        LDA #0
  2600        STA CALL+1
  2610        STA CALL
  2620 .1     JSR NEXT.CHAR
  2630        BEQ .2            EOL
  2640        SEC
  2650        SBC #'0           make value
  2660        BCC .2            <0 isn't number
  2670        CMP #9+1
  2680        BCS .2            >9 isn't number
  2690        PHA               save value
  2700        LDA CALL
  2710        STA TEMP
  2720        LDA CALL+1        multiply CALL * 10
  2730        ASL
  2740        ROL TEMP
  2750        ASL
  2760        ROL TEMP
  2770        ADC CALL+1
  2780        STA CALL+1
  2790        LDA TEMP
  2800        ADC CALL
  2810        STA CALL
  2820        ASL CALL+1
  2830        ROL CALL
  2840        PLA               get value this digit
  2850        ADC CALL+1        and add it in
  2860        STA CALL+1
  2870        BCC .1
  2880        INC CALL
  2890        BCS .1         ...always
  2910 .2     LDA PNTR          back up PNTR
  2920        BNE .3
  2930        DEC PNTR+1
  2940 .3     DEC PNTR
  2950        RTS
  2960 *--------------------------------
  2970 NEXT.CHAR
  2980        LDY #0
  2990        LDA (PNTR),Y
  3000        BEQ .1            EOL
  3010        INC PNTR          bump pointer
  3020        BNE .1
  3030        INC PNTR+1
  3040 .1     RTS
  3050 *--------------------------------
  3070        LDA CALL          hi-byte of called line
  3080        AND #$FC          hi 6 bits
  3090        LSR               make 0-126
  3100        ADC #HSHTBL       carry is clear
  3110        STA STPNTR
  3120        LDA /HSHTBL
  3130        ADC #0
  3140        STA STPNTR+1
  3150 *--- fall into CHAIN.SEARCH routine
  3160 *--------------------------------
  3180 .1     LDY #0            point at pointer in entry
  3190        LDA (STPNTR),Y
  3200        STA TPTR
  3210        INY
  3220        LDA (STPNTR),Y
  3230        BEQ .4            end of chain, not in table
  3240        STA TPTR+1
  3250        LDX #2            2 bytes in number
  3260        LDY #2            point at line number in entry
  3270 .2     LDA (TPTR),Y      compare numbers
  3280        CMP ENTRY,Y
  3290        BCC .3            not this one, but keep looking
  3300        BNE .4            not in this chain
  3310        DEX
  3320        BEQ .5            same number
  3330        INY               next byte pair
  3340        BNE .2         ...always
  3360 .3     JSR .5            update pointer, clear carry
  3370        BCC .1         ...always
  3390 .4     SEC               did not find
  3400        RTS
  3420 .5     LDA TPTR          point to matching entry
  3430        STA STPNTR
  3440        LDA TPTR+1
  3450        STA STPNTR+1
  3460        CLC
  3470        RTS
  3480 *--------------------------------
  3500        CLC               adjust pointer to start
  3510        LDA STPNTR        of line # chain
  3520        ADC #4
  3530        STA ENTRY
  3540        LDA STPNTR+1
  3550        ADC #0
  3560        STA ENTRY+1
  3570        LDA #ENTRY
  3580        STA STPNTR
  3590        LDA /ENTRY
  3600        STA STPNTR+1
  3610        LDA LINNUM        put line number into symbol
  3620        STA ENTRY+3
  3630        LDA LINNUM+1
  3640        STA ENTRY+2
  3650        JMP CHAIN.SEARCH
  3660 *--------------------------------
  3680        STA SIZE
  3690        CLC               see if room
  3700        LDX #1
  3710        LDY #0
  3720        STY SIZE+1
  3730 .1     LDA (STPNTR),Y    get current pointer
  3740        STA ENTRY,Y       into new entry
  3750        LDA EOT,Y         point old entry
  3760        STA (STPNTR),Y    to this one
  3770        STA TPTR,Y
  3780        ADC SIZE,Y        and adjust end-of-table
  3790        STA EOT,Y
  3800        INY
  3810        DEX
  3820        BPL .1            now do low-bytes
  3830 *--- see if there's going to be enough room
  3840        LDA EOT
  3850        CMP #LCR
  3860        LDA EOT+1
  3870        SBC /LCR
  3880        BCS .3            MEM FULL error
  3890 *--- move entry into call table
  3900        LDY SIZE
  3910        DEY
  3920 .2     LDA ENTRY,Y
  3930        STA (TPTR),Y
  3940        DEY
  3950        BPL .2
  3960        LDA TPTR
  3970        STA STPNTR
  3980        LDA TPTR+1
  3990        STA STPNTR+1
  4000        RTS
  4020 .3     JMP AS.MEMFULL    abort with error message
  4030 *--------------------------------
  4050        LDA PRGBOT
  4060        STA PNTR          start defined line search
  4070        LDA PRGBOT+1      at beginning of program
  4080        STA PNTR+1
  4090        LDA #0            start at chain 0
  4100 .1     STA TEMP
  4110        ASL
  4120        TAY
  4130        LDA HSHTBL+1,Y
  4140        BEQ .2            no entries for this chain
  4150        STA STPNTR+1
  4160        LDA HSHTBL,Y
  4170        STA STPNTR
  4180        JSR PRINT.CHAIN
  4190 .2     INC TEMP
  4200        LDA TEMP
  4210        CMP #$40
  4220        BCC .1            still more chains
  4230        RTS               finished
  4240 *--------------------------------
  4260        JSR CHECK.FOR.PAUSE
  4270        BEQ .1            <CR> abort
  4280        LDY #2
  4290        LDA (STPNTR),Y
  4300        STA LINNUM+1
  4310        INY
  4320        LDA (STPNTR),Y
  4330        STA LINNUM
  4360        LDA DEFFLAG       "*" or " "
  4370        JSR MON.COUT
  4380        CLC
  4390        LDA STPNTR
  4400        ADC #4            point at line # chain
  4410        STA TPTR
  4420        LDA STPNTR+1
  4430        ADC #0
  4440        STA TPTR+1
  4460        JSR MON.CROUT
  4470        LDY #1
  4480        LDA (STPNTR),Y    pointer to next call
  4490        BEQ .2            no more
  4500        PHA
  4510        DEY
  4520        LDA (STPNTR),Y
  4530        STA STPNTR
  4540        PLA
  4550        STA STPNTR+1
  4560        BNE PRINT.CHAIN  ...always
  4580 .1     PLA               return to top level
  4590        PLA               if <CR> abort
  4600 .2     RTS
  4610 *--------------------------------
  4630        LDY #3
  4640        LDX #1
  4650 .1     LDA (PNTR),Y      look at next line in program
  4660        CMP LINNUM,X
  4670        BCC .4            < our number, get new line
  4680        BNE .2            >  "    "   , not defined
  4690        DEY               =  "    "   , go on
  4700        DEX               now do low order bytes
  4710        BPL .1
  4720        LDA #" "          found it!
  4730        BNE .3         ...always
  4750 .2     LDA #"*"          flag undefined line
  4760 .3     STA DEFFLAG
  4770        RTS
  4790 .4     LDY #1
  4800        LDA (PNTR),Y      hi-byte of next line address
  4805        BEQ .2
  4810        PHA
  4820        DEY
  4830        LDA (PNTR),Y      and lo-byte
  4840        STA PNTR
  4850        PLA
  4860        STA PNTR+1
  4880 *--------------------------------
  4900        LDA #0            reset counter to 0
  4910        STA COUNTER       for each call
  4920 .1     JSR TAB.NEXT.COLUMN
  4930        LDY #2            point at line #
  4940        LDA (TPTR),Y
  4950        STA LINNUM+1
  4960        INY
  4970        LDA (TPTR),Y
  4980        STA LINNUM
  5000        LDY #1            set up next pointer
  5010        LDA (TPTR),Y
  5020        BEQ .2            end of chain
  5030        PHA
  5040        DEY
  5050        LDA (TPTR),Y
  5060        STA TPTR
  5070        PLA
  5080        STA TPTR+1
  5090        BNE .1         ...always
  5110 .2     RTS
  5120 *--------------------------------
  5140        JSR MON.CROUT
  5170 .1     LDA #7            first tab stop
  5180 .2     CMP MON.CH        cursor position
  5190        BCS .3            perform tab
  5200        ADC #6            next tab stop
  5210        CMP #33           end of line?
  5220        BCC .2
  5230        INC COUNTER       count the screen line
  5240        LDA COUNTER
  5250        AND #1            look at odd-even bit
  5260        BEQ TAB.NEW.LINE  both scrn and printer
  5270        LDA #CR
  5280        JSR MON.COUT1     <CR> to screen only
  5290        JMP .1         ...always
  5310 .3     BEQ .4            already there
  5320        SBC MON.CH        calculate # of blanks
  5330        TAX
  5340        JSR MON.PRBL2
  5350 .4     RTS
  5360 *--------------------------------
  5380        LDX #4            print 5 digits
  5390        STX LZFLAG        turn on leading zero flag
  5400 .1     LDA #'0           digit=0
  5410 .2     PHA
  5420        SEC
  5430        LDA LINNUM
  5440        SBC PLNTBL,X
  5450        PHA
  5460        LDA LINNUM+1
  5470        SBC PLNTBH,X
  5480        BCC .3            less than divisor
  5490        STA LINNUM+1
  5500        PLA
  5510        STA LINNUM
  5520        PLA
  5530        ADC #0            increment digit
  5540        BNE .2         ...always
  5560 .3     PLA
  5570        PLA
  5580        CMP #'0
  5590        BEQ .5            zero, might be leading
  5600        SEC               turn off LZFLAG
  5610        ROR LZFLAG
  5620 .4     ORA #$80
  5630        JSR MON.COUT
  5640        DEX
  5650        BPL .1
  5660        RTS
  5670 .5     BIT LZFLAG        leading zero flag
  5680        BMI .4            no
  5690        CPX #0            if all zeroes, print one
  5700        BEQ .4
  5710        LDA #'            blank
  5720        BNE .4         ...always
  5740 PLNTBL .DA #1
  5750        .DA #10
  5760        .DA #100
  5770        .DA #1000
  5780        .DA #10000
  5790 PLNTBH .DA /1
  5800        .DA /10
  5810        .DA /100
  5820        .DA /1000
  5830        .DA /10000
  5840 *--------------------------------
  5860        LDA KEYBOARD      keypress?
  5870        BPL .2            no, go on
  5880        STA STROBE
  5890        CMP #CR           RETURN?
  5900        BEQ .2            yes
  5910 .1     LDA KEYBOARD      no, wait for
  5920        BPL .1            another stroke
  5930        STA STROBE
  5940        CMP #CR           return .EQ. if RETURN
  5950 .2     RTS
  5960 *--------------------------------

Speaking of Slow Chips Robert H. Bernard

William O'Ryan's article (AAL June 1984) about making the 65C02 work in II+s reminds me of some other slow chip problems I have had in the past with Apples.

Years ago, I had a problem with an SSM AIO card in an Apple that traced to a slow 74LS138 at position H2. The symptom was that every few hours the program would fly off into the weeds. I traced it to the device select for the slot, which caused the data on the bus to be late for ROM program fetches from the card. I was able to fix the problem in that case by swapping H2 with another '138 from a different (less critical) position.

Some time later I was able to fix a problem in another machine by swapping the ROM SELECT chip at position F12 (another 74LS138) with another '138. There are apparently many marginal timing situations in II+s, and they are not necessarily in the oldest ones.

All this slow circuit stuff has some interesting side effects. I personally had a number of conversations with SSM about this problem before I found the real cause, and all they could suggest was a capacitor on the clock line. Even after I found the problem, the SSM people I talked to seemed uninterested in the fix, perhaps because they couldn't apply it directly to their product.

The unfortunate end result was that a number of organizations that previously sold or recommended AIO cards stopped doing so. A domino effect was that our local retailer stopped pushing Anadex printers (which required the DTR signal, at that time only available on the AIO) rather than find another serial card to replace the AIO. I always wondered if the Anadex people noticed the effect on their sales....

Modify DOS 3.3 for Big BSAVEs Bob Sander-Cederlof

Jim Sather (author of "Understanding the Apple II" and designer of the QuikLoader card) called today, and one topic of discussion was DOS 3.3's limit of 32767 for the maximum size of a binary file. Jim has been blowing 27256 EPROMs, which are 32768 bytes long. To write a whole EPROMs worth of code on disk it takes two files, because the EPROM holds one more byte than the maximum size file.

The limit doesn't apply if you write the file with the .TF directive in the S-C Macro Assembler, but it is checked when you type in a BSAVE command. The "L" parameter must be less than 32768.

I remembered that somewhere very recently I had read of a quick patch to DOS to remove the restriction. Where? Hardcore Computing? Call APPLE? Washington Apple Pi?

The answer was "yes" to both Call APPLE and W.A.P., because Bruce Field's excellent Apple Doctor column is printed in both magazines. The July 1984 Call APPLE, on page 55, has the answer:

"Sure, change memory location $A964 in DOS from $7F to $FF. From Applesoft this can be done with POKE 43364,255. This changes the range attribute table in DOS to allow binary files as large as 65535 bytes."

By the way, please do not try to BSAVE 65535 bytes on one file. You will not succeed, because doing so will involve saving bytes from the $C000-C0FF range. This is where all the I/O soft switches are, any you will drive your Apple and peripherals wild. And you will not be able to BLOAD it, because it will load on top of the DOS buffers. In general, do not BSAVE any area of RAM which includes $C000-C0FF. Do not BLOAD into the DOS buffers or DOS variables.

If you want to test Bruce's patch, make the patch and then BSAVE filename,A$800,L$8E00. This will save from $800 through $95FF.

Apple Assembly Line is published monthly by S-C SOFTWARE CORPORATION, P.O. Box 280300, Dallas, Texas 75228. Phone (214) 324-2050. Subscription rate is $18 per year in the USA, sent Bulk Mail; add $3 for First Class postage in USA, Canada, and Mexico; add $12 postage for other countries. Back issues are available for $1.80 each (other countries add $1 per back issue for postage).

All material herein is copyrighted by S-C SOFTWARE, all rights reserved. Unless otherwise indicated, all material herein is authored by Bob Sander-Cederlof. (Apple is a registered trademark of Apple Computer, Inc.)