LLX > Neil Parker > Apple II > Addons for Applesoft

Addons for Applesoft

Here are a few machine-language snippets that add useful new statements and functions to Applesoft. They come in two varieties: new statements, added through Applesoft's ampersand (&) vector, and new functions, added through the lesser-known and often-neglected USR vector.

Though the assembly listings below all start at memory location $300, they are all fully relocatable, and may be positioned anywhere in memory without change. Only the address to be POKEed into the ampersand or USR vector would change.

Additionally, the ampersand routines are chainable - if you want to use more than one of them at a time, just position each one so that its first byte overwrites the RTS at the end of the previous one.

Unfortunately, the USR functions are not as easily chainable. USR wasn't designed to allow more than one user-defined function at a time (not that it can't be done, but doing it requires jumping through some additional hoops).

Actually setting up the ampersand or USR vector isn't illustrated below. If you load an ampersand routine at $300, connect it up like this:

 POKE 1013,76: POKE 1014,0: POKE 1015,3

Setting up a USR routine is similar:

 POKE 10,76: POKE 11,0: POKE 12,3

The 0 and the 3 will change depending on where you load the routine, but the 76 ($4C, the 6502 JMP instruction) is always constant.

(If there's any doubt about which setup to use, look at the syntax example below the code.)

License: The code below may be used by anyone at any time for any purpose with no restrictions whatsoever. Though I would appreciate an acknowledgement if you find any of these useful, it's not required. There is, of course, no warranty - I cannot claim that any of them is bug-free, or that you will find any of them useful, nor can I provide any form of formal support or assume any legal liability.

Input (Almost) Anything

Applesoft's INPUT statement really isn't designed for human beings to interact with - its parsing rules are somewhat complex, and if you want to respond to an INPUT with a string that contains both commas and quotation marks in it, you're out of luck: it's impossible.

The code below adds a statement similar to the LINE INPUT found in many other BASICs. You can read into a string variable almost any ASCII characters, up to 255 characters - the only ASCII characters that can't be read as data are the standard Apple input-editing keys (Return, Backspace, Forward Arrow, Escape, and Delete - CHR$'s 13, 8, 21, 27, and 127).

                1    INPUTTKN =     $84        ;Asoft INPUT token
                2    PROMPT   =     $33        ;Prompt char for GETLN
                3    FORPNT   =     $85        ;Var ptr for GETSPT
                4    IN       =     $200       ;Buffer for GETLN
                5    CHRGET   =     $B1        ;Get next program token
                6    GDBUFS   =     $D539      ;Mask off hi bits of input
                7    GETSPT   =     $DA7B      ;Assign FAC to str var
                8    STRPRT   =     $DB3D      ;Print string in FAC
                9    CHKSTR   =     $DD6C      ;TYPE MISMATCH if not string
                10   STRTXT   =     $DE81      ;Parse string from prog text
                11   SYNCHR   =     $DEC0      ;SYNTAX err if next token<>A
                12   PTRGET   =     $DFE3      ;Parse var name, find in mem
                13   ERRDIR   =     $E306      ;ILLEGAL DIRECT if not running
                14   STRSPA   =     $E3DD      ;Get space for new str
                15   PUTNEW   =     $E42A      ;Make temp str descript
                16   MOVSTR   =     $E5E2      ;Move data to str space
                17   GETLN    =     $FD6A      ;Read line of input
                18            ORG   $300
0300: C9 84     19            CMP   #INPUTTKN  ;Is it INPUT?
0302: D0 3E     20            BNE   OUT        ;Skip if not
0304: 20 06 E3  21            JSR   ERRDIR     ;Make sure prog is running
0307: 20 B1 00  22            JSR   CHRGET     ;Get next token
030A: C9 22     23            CMP   #$22       ;Quote?
030C: D0 0B     24            BNE   NOPROMPT   ;Don't prompt if not
030E: 20 81 DE  25            JSR   STRTXT     ;Parse str const
0311: 20 3D DB  26            JSR   STRPRT     ;Print it
0314: A9 3B     27            LDA   #';'       ;Check for ';'
0316: 20 C0 DE  28            JSR   SYNCHR
0319: 20 E3 DF  29   NOPROMPT JSR   PTRGET     ;Parse var name
031C: 85 85     30            STA   FORPNT     ;Save its address
031E: 84 86     31            STY   FORPNT+1
0320: 20 6C DD  32            JSR   CHKSTR     ;Error if not string
0323: A9 80     33            LDA   #$80       ;No prompt
0325: 85 33     34            STA   PROMPT
0327: 20 6A FD  35            JSR   GETLN      ;Get input
032A: 8A        36            TXA              ;Save its length
032B: 48        37            PHA
032C: 20 39 D5  38            JSR   GDBUFS     ;Chop off hi bits
032F: 68        39            PLA              ;Get saved len
0330: 48        40            PHA
0331: 20 DD E3  41            JSR   STRSPA     ;Get space for str
0334: A0 02     42            LDY   #>IN
0336: A2 00     43            LDX   #<IN
0338: 68        44            PLA
0339: 20 E2 E5  45            JSR   MOVSTR     ;Move input to string space
033C: 20 2A E4  46            JSR   PUTNEW     ;Make new temp descriptor
033F: 4C 7B DA  47            JMP   GETSPT     ;Assign descriptor to var
0342: 60        48   OUT      RTS


--End assembly, 67 bytes, Errors: 0

The syntax is:

 &  INPUT ["prompt string";] string-variable

The prompt is optional; if present, it must be a literal quoted string followed by a semicolon. If it's not present, no prompt at all is printed - if you want a question mark prompt like the one Applesoft's INPUT provides, say something like & INPUT "?";A$.

You only get one variable per & INPUT statement. If you want to input two or more variables, use two or more & INPUT statements.

Input (Almost) Anything, Again

Here's another, rather different way to do the same thing:

                1    PROMPT   =     $33        ;Prompt char for GETLN
                2    IN       =     $200       ;Buffer for GETLN
                3    GDBUFS   =     $D539      ;Mask off hi bits of input
                4    STRPRT   =     $DB3D      ;Print string in FAC
                5    CHKSTR   =     $DD6C      ;TYPE MISMATCH if not string
                6    ERRDIR   =     $E306      ;ILLEGAL DIRECT if not running
                7    STRSPA   =     $E3DD      ;Get space for new str
                8    PUTNEW   =     $E42A      ;Make temp str descript
                9    MOVSTR   =     $E5E2      ;Move data to str space
                10   GETLN    =     $FD6A      ;Read line of input
                11            ORG   $300
0300: 20 06 E3  12            JSR   ERRDIR     ;Err if prog not running
0303: 20 6C DD  13            JSR   CHKSTR
0306: 20 3D DB  14            JSR   STRPRT     ;Print arg
0309: A9 80     15            LDA   #$80       ;No prompt
030B: 85 33     16            STA   PROMPT
030D: 20 6A FD  17            JSR   GETLN      ;Get input
0310: 8A        18            TXA              ;Save its length
0311: 48        19            PHA
0312: 20 39 D5  20            JSR   GDBUFS     ;Chop off hi bits
0315: 68        21            PLA              ;Get saved len
0316: 48        22            PHA
0317: 20 DD E3  23            JSR   STRSPA     ;Get space for str
031A: A0 02     24            LDY   #>IN
031C: A2 00     25            LDX   #<IN
031E: 68        26            PLA
031F: 20 E2 E5  27            JSR   MOVSTR     ;Move input to string space
0322: 68        28            PLA              ;Must pop 1 return addr
0323: 68        29            PLA              ; before returning a string
0324: 4C 2A E4  30            JMP   PUTNEW     ;Return new temp descriptor


--End assembly, 39 bytes, Errors: 0

The syntax is:

 USR (string-expression)

This is a function that returns a string, and can be used anywhere in an expression where a string-valued function would be legal. The argument is printed as a prompt, and in this case you can use any string expression at all, not just a literal quoted string. For example:

100 P$ = "Yes, Master?"
110 A$ = USR (P$ + " ")
120  PRINT "You typed "; A$

If you want no prompt at all, pass an empty string, for example:

200 A$ =  USR ("")

Computed GOTO

One feature that old Integer BASIC programmers often missed in Applesoft is the ability to GOTO any numeric expression, not just a literal line number. Applesoft does have ON ... GOTO, but it's just not the same.

Adding an Integer-like computed GOTO to Applesoft is particularly easy:

                1    GOTOTKN  =     $AB        ;Asoft GOTO token
                2    CHRGET   =     $B1        ;Get next program token
                3    GOTO     =     $D93E      ;Parse & run GOTO stmt
                4    FRMNUM   =     $DD67      ;Eval numeric expr
                5    GETADR   =     $E752      ;Convert to 2-byte int
                6             ORG   $300
0300: C9 AB     7             CMP   #GOTOTKN   ;Is it GOTO?
0302: D0 0C     8             BNE   OUT        ;Skip if not
0304: 20 B1 00  9             JSR   CHRGET     ;Get next token
0307: 20 67 DD  10            JSR   FRMNUM     ;Eval numeric expr
030A: 20 52 E7  11            JSR   GETADR     ;Convert to int
030D: 4C 41 D9  12            JMP   GOTO+3     ;Find line in mem & goto it
0310: 60        13   OUT      RTS


--End assembly, 17 bytes, Errors: 0

Syntax:

 &  GOTO numeric-expression

Of course the expression must evaluate to an actual line number in your program, or you'll get ?UNDEF'D STATEMENT ERROR.

The use of the GETADR routine (the same routine used to get the memory address for PEEK, POKE, and CALL) has an odd side effect: negative line numbers are accepted. The routine will make it positive by adding 65536 to it.

Computed GOSUB

Integer BASIC also had computed GOSUBs. Here it is for Applesoft:

                1    GOSUBTKN =     $B0        ;Asoft GOSUB token
                2    CURLIN   =     $75        ;Current line no.
                3    TXTPTR   =     $B8        ;Current token addr
                4    CHRGET   =     $B1        ;Get next program token
                5    GETSTK   =     $D3D6      ;Check stack space
                6    NEWSTT   =     $D7D2      ;Execute statements
                7    GOTO     =     $D93E      ;Go to new line no.
                8    FRMNUM   =     $DD67      ;Eval numeric expression
                9    GETADR   =     $E752      ;Convert number to 2-byte int
                10            ORG   $300
0300: C9 B0     11            CMP   #GOSUBTKN  ;Is it GOSUB?
0302: D0 23     12            BNE   OUT        ;Skip if not
0304: A9 03     13            LDA   #3         ;Make sure there's enough stack
0306: 20 D6 D3  14            JSR   GETSTK
0309: A5 B9     15            LDA   TXTPTR+1   ;Push marker for RETURN
030B: 48        16            PHA
030C: A5 B8     17            LDA   TXTPTR
030E: 48        18            PHA
030F: A5 76     19            LDA   CURLIN+1
0311: 48        20            PHA
0312: A5 75     21            LDA   CURLIN
0314: 48        22            PHA
0315: A9 B0     23            LDA   #GOSUBTKN
0317: 48        24            PHA
0318: 20 B1 00  25            JSR   CHRGET     ;Get next token
031B: 20 67 DD  26            JSR   FRMNUM     ;Parse numeric expr
031E: 20 52 E7  27            JSR   GETADR     ;Convert it to int
0321: 20 41 D9  28            JSR   GOTO+3     ;Point at chosen statement
0324: 4C D2 D7  29            JMP   NEWSTT     ;Start running it
0327: 60        30   OUT      RTS


--End assembly, 40 bytes, Errors: 0

Syntax:

 &  GOSUB numeric-expression

Again, the expression must evaluate to an actual line number, or you'll get ?UNDEF'D STATEMENT ERROR. Negative line numbers are accepted just as with computed GOTO.

RESTORE to Arbitrary (Computed) Line Number

It's sometimes handy to be able to RESTORE to a DATA statement other than the first one in the program, and many other BASICs allow it. This adds it to Applesoft:

                1    RESTORETKN =   $AE        ;Asoft RESTORE token
                2    DATPTR   =     $7D        ;DATA stmt pointer
                3    LOWTR    =     $9B        ;FNDLIN puts link ptr here
                4    CHRGET   =     $B1        ;Get next program token
                5    FNDLIN   =     $D61A      ;Find line in memory
                6    FRMNUM   =     $DD67      ;Evaluate a numeric expression
                7    GETADR   =     $E752      ;Convert number to 2-byte int
                8             ORG   $300
0300: C9 AE     9             CMP   #RESTORETKN ;Is it RESTORE?
0302: D0 19     10            BNE   OUT        ;Skip if not
0304: 20 B1 00  11            JSR   CHRGET     ;Get next token
0307: 20 67 DD  12            JSR   FRMNUM     ;Eval expression
030A: 20 52 E7  13            JSR   GETADR     ;Convert to int
030D: 20 1A D6  14            JSR   FNDLIN     ;Find chosen line no.
0310: A4 9C     15            LDY   LOWTR+1    ;Point DATPTR at byte before it
0312: A6 9B     16            LDX   LOWTR
0314: D0 01     17            BNE   ND
0316: 88        18            DEY
0317: CA        19   ND       DEX
0318: 84 7E     20            STY   DATPTR+1
031A: 86 7D     21            STX   DATPTR
031C: 60        22            RTS
031D: 60        23   OUT      RTS


--End assembly, 30 bytes, Errors: 0

Syntax:

 &  RESTORE numeric-expression

After executing & RESTORE, the next READ will start searching for DATA statements at the indicated line number.

The line number can be any numeric expression. It does not need to evaluate to a line number that actually exists in your program; if there is no such line number, READ will start searching at the next higher line number that does actually exist.

Negative line number are accepted just as with & GOTO and & GOSUB.

The strange double-RTS at the end of the code is intended to make this routine chainable in the same manner as the other ampersand routines. If you don't intend to chain any other ampersand routines after this one, you can save a byte by deleting the last RTS, and changing the fourth byte from $19 to $18.

ON ... RESTORE

For those who prefer their computed RESTORE to work in a more classically Applesoft-like manner, here's an ON ... RESTORE statement that works like ON ... GOTO and ON ... GOSUB:

                1    RESTORETKN =   $AE        ;Asoft RESTORE token
                2    ONTKN    =     $B4        ;Asoft ON token
                3    DATPTR   =     $7D        ;DATA stmt pointer
                4    LOWTR    =     $9B        ;FNDLIN puts link ptr here
                5    FACLO    =     $A1        ;GETBYT puts result here
                6    CHRGET   =     $B1        ;Get next program token
                7    FNDLIN   =     $D61A      ;Find line in memory
                8    DATA     =     $D995      ;DATA stmt handler - skip to end of stmt
                9    LINGET   =     $DA0C      ;Parse line number
                10   SNERR    =     $DEC9      ;Fail with SYNTAX err
                11   GETBYT   =     $E6F8      ;Evaluate expr in range 0..255
                12            ORG   $300
0300: C9 B4     13            CMP   #ONTKN     ;Is it ON?
0302: D0 34     14            BNE   OUT        ;Skip if not
0304: 20 B1 00  15            JSR   CHRGET     ;Get next token
0307: 20 F8 E6  16            JSR   GETBYT     ;Eval expr, ILLEGAL QUANTITY if not in 0..255
030A: C9 AE     17            CMP   #RESTORETKN ;Is next token RESTORE?
030C: F0 03     18            BEQ   COUNT      ;Continue if so
030E: 4C C9 DE  19            JMP   SNERR      ;Else SYNTAX err
0311: C6 A1     20   COUNT    DEC   FACLO      ;Skipped enough line nums yet?
0313: D0 18     21            BNE   NXTNUM     ;If not, go skip another
0315: 20 B1 00  22            JSR   CHRGET     ;Else advance to 1st digit
0318: 20 0C DA  23            JSR   LINGET     ;Parse it
031B: 20 1A D6  24            JSR   FNDLIN     ;Find it in memory
031E: A4 9C     25            LDY   LOWTR+1    ;Point DATPTR at byte before it
0320: A6 9B     26            LDX   LOWTR
0322: D0 01     27            BNE   DX
0324: 88        28            DEY
0325: CA        29   DX       DEX
0326: 84 7E     30            STY   DATPTR+1
0328: 86 7D     31            STX   DATPTR
032A: 4C 95 D9  32            JMP   DATA       ;Skip over any remaining line nums & exit
032D: 20 B1 00  33   NXTNUM   JSR   CHRGET     ;Skipping - advance to 1st digit
0330: 20 0C DA  34            JSR   LINGET     ;Parse line num (& ignore it)
0333: C9 2C     35            CMP   #','       ;Followed by comma?
0335: F0 DA     36            BEQ   COUNT      ;If so, check if next line num is right
0337: 60        37            RTS              ;Else we're done.
0338: 60        38   OUT      RTS


--End assembly, 57 bytes, Errors: 0

Syntax:

 &  ON numeric-expression RESTORE linenum[,linenum[,...]]

As with Applesoft's ON ... GOTO/GOSUB, the numeric expression is any expression that evaluates to a number in the range 0 to 255. The linenums are a list of literal line numbers (no expressions allowed) separated by commas, and the numeric expression selects which line number to RESTORE to: if the expression is 1, the first line number is used, and if the expression is 2, the second line number is used, and so on. If the expression is 0 or greater than the number of line numbers, no RESTORE is done, and no error occurs.

If the expression is less than zero or greater than 255, ?ILLEGAL QUANTITY ERROR occurs.

The line number chosen need not actually exist in the program. If it doesn't, READ will start searching at the next higher line that does exist.

The strange double-RTS at the end of the code is intended to make this routine chainable in the same manner as the other ampersand routines. If you don't intend to chain any other ampersand routines after this one, you can save a byte by deleting the last RTS, and changing the fourth byte from $34 to $33.

Two-byte PEEK

Sometimes it's convenient to deal with data PEEKed from memory in two-byte quantities. Here's a routine to help with that:

                1    LINNUM   =     $50        ;Result from GETADR
                2    CHKNUM   =     $DD6A      ;Make sure expr is number
                3    GIVAYF   =     $E2F2      ;Float signed int in A,Y
                4    GETADR   =     $E752      ;Convert number to int
                5             ORG   $300
0300: 20 6A DD  6             JSR   CHKNUM     ;TYPE MISMATCH if not number
0303: A5 51     7             LDA   LINNUM+1   ;Preserve LINNUM
0305: 48        8             PHA              ;(Not sure why, but PEEK does it, so I do too)
0306: A5 50     9             LDA   LINNUM
0308: 48        10            PHA
0309: 20 52 E7  11            JSR   GETADR     ;Convert num to int
030C: A0 01     12            LDY   #1         ;Get value from mem into X,Y
030E: B1 50     13            LDA   (LINNUM),Y
0310: AA        14            TAX
0311: 88        15            DEY
0312: B1 50     16            LDA   (LINNUM),Y
0314: A8        17            TAY
0315: 68        18            PLA              ;Restore saved LINNUM
0316: 85 50     19            STA   LINNUM
0318: 68        20            PLA
0319: 85 51     21            STA   LINNUM+1
031B: 8A        22            TXA
031C: 4C F2 E2  23            JMP   GIVAYF     ;Float result


--End assembly, 31 bytes, Errors: 0

Syntax:

 USR (numeric-expression)

The argument must evaluate to a number from 0 to 65535 (or equivalently, -65536 to -1), which is interpreted as a memory address. W = USR (A) is roughly equivalent to W = PEEK (A) + 256 * PEEK (A + 1), except that runs faster and with fewer tokens, and it returns its result as a signed number (in the range -32768 ... 32767). If you need a positive result, you can get it like this:

 W = USR (A): IF W < 0 THEN W = W + 65536

Two-byte POKE

Here's the inverse of Two-byte PEEK - a statement that POKEs a two-byte number into memory:

                1    POKETKN  =     $B9        ;Asoft POKE token
                2    LINNUM   =     $50        ;Result from GETADR
                3    FORPNT   =     $85        ;Temp pointer
                4    CHRGET   =     $B1        ;Get next program token
                5    FRMNUM   =     $DD67      ;Evaluate a numeric expression
                6    CHKCOM   =     $DEBE      ;SYNTAX err if not comma
                7    GETADR   =     $E752      ;Convert num to 2-byte int
                8             ORG   $300
0300: C9 B9     9             CMP   #POKETKN   ;Is it POKE?
0302: D0 2A     10            BNE   OUT        ;Skip if not
0304: 20 B1 00  11            JSR   CHRGET     ;Get next token
0307: 20 67 DD  12            JSR   FRMNUM     ;Eval expression
030A: 20 52 E7  13            JSR   GETADR     ;Convert to int
030D: A5 51     14            LDA   LINNUM+1   ;Save it
030F: 48        15            PHA
0310: A5 50     16            LDA   LINNUM
0312: 48        17            PHA
0313: 20 BE DE  18            JSR   CHKCOM     ;Check for comma
0316: 20 67 DD  19            JSR   FRMNUM     ;Eval expression
0319: 20 52 E7  20            JSR   GETADR     ;Convert to int
031C: 68        21            PLA              ;Get saved number
031D: 85 85     22            STA   FORPNT
031F: 68        23            PLA
0320: 85 86     24            STA   FORPNT+1
0322: A0 00     25            LDY   #0         ;Store 2 bytes in memory
0324: A5 50     26            LDA   LINNUM
0326: 91 85     27            STA   (FORPNT),Y
0328: C8        28            INY
0329: A5 51     29            LDA   LINNUM+1
032B: 91 85     30            STA   (FORPNT),Y
032D: 60        31            RTS
032E: 60        32   OUT      RTS


--End assembly, 47 bytes, Errors: 0

Syntax:

 &  POKE numeric-expression,numeric-expression

Each argument must evaluate to a number from 0 to 65535 (or equivalently, -65536 to -1). The first argument is a memory address, and the second is a value to be POKEed into that address. & POKE A,W is roughly equivalent to WH = INT (W / 256): WL = W - WH * 256: POKE A,WL: POKE A + 1,WH, except that it runs faster and with fewer tokens, and doesn't use any temporary variables, and it handles negative values properly.

The strange double-RTS at the end of the code is intended to make this routine chainable in the same manner as the other ampersand routines. If you don't intend to chain any other ampersand routines after this one, you can save a byte by deleting the last RTS, and changing the fourth byte from $2A to $29.

LLX > Neil Parker > Apple II > Addons for Applesoft

Original: June 2, 2016
Modified February 26, 2017--added ON ... RESTORE