Three Ideas: Handy Tools for Business BASIC

Text by Dave. Lingwood
Programs by Brian Matthews (Action-Research NW)

BASIC COMPARISONS
Most Apple /// Business BASIC users also work with Applesoft, either from earlier ][ days, or through emulation mode. Business BASIC (hereafter “BB”) has all the professional features you need, but it lacks the flexibility provided by the openness of the ][. This article and attached programs recount some of the pitfalls we encountered and useful tricks devised in transferring a large statistical analysis package from Applesoft to BB. The tricks add back some of the flexibility of Applesoft.

1: ARRAY HANDLING AND REQUEST.INV
If you haven’t already discovered it, REQUEST.INV, provided with BB, contains very useful routines for rapid reading and saving of numeric data arrays. They are FILREAD and FILWRITE. FILREAD/FILWRITE do a read or save directly into/from a vector. The data are saved as a binary file on the disk. The PERFORM command tells the invokable the name of the array or vector, the file name, and the number of bytes to transfer. Let’s assume you have an array DIMensioned as DD%(N) which contains data you want to save save to disk, then re-fill with other data on the disk. Here are the appropriate BB statements to do it:
10 INVOKE”request.inv”:REM at beginning of program
20 DIM RW$(1): RW$(0)= “Read”: RW$(1)= “Writ”
100 filename$= “dataout”: RW= 1: REM write data
110 GOSUB 1000
200 filename$= “datain” RW= 0: REM read data
210 GOSUB 1000
1000 REM Subroutine for array read/write. RW=0 to read, =1 to write
1010 PRINT RW$(RW); “ing array DD%”
1020 nbytes%= (N+1) * 2:REM 2 since integer array
1030 datafile%= filenumb: REM # of data file
1040 DD$= “DD%”: REM name of array to transfer
1050 OPEN#datafile%,filename$
1060 IF RW THEN PERFORM FILWRITE(%datafile%,@DD$,%nbytes%)
1070 IF NOT RW THEN PERFORM FILREAD(%datafile%,@DD$,%nbytes%,@count%): IF nbytes%<>count% THEN PRINT “Err: Read file <> array length!”
1080 CLOSE#datafile%
1090 RETURN

The RW variable controls reading or writing. DD$ is the important value passed to FILREAD or FILWRITE which tells it the name of the array to find in memory, and from the beginning (0th cell) of which to begin reading or writing “nbytes%” bytes. The “count%” variable is returned after a read, containing the number of bytes actually read. The IF test after the read checks that this figure agrees with the number of bytes in the array (though you might want to read fewer bytes into an array, you should never read more!).

You could even put the names of various arrays into a string array, then assign DD$=ARRAY$(k), for example, or pass the array name into the subroutine as a parameter. There is a lot of flexibility here, and more importantly, a lot of SPEED. Data transfer is much faster than with INPUT/PRINT.

2: DIMENSIONS, STRINGS AND THE 256K ///
A problem we bumped into almost immediately with the stat package was the limit on array size, and the rule about string arrays in BB that affects the 256K machine.

Simply put:
a. No numeric array can be longer than 64K
b. Strings must be defined in the first 64K block of program memory.

The first, while a pain, you can live with by careful program design (especially with FILREAD/FILWRITE available). The second causes real speed problems if you have many string arrays. Being an interpreter, BB has to scan through the list of all arrays to find the one you want. Normally you’d define your fast numeric arrays first; but if your numeric array is a full 64K in size, that would force any string arrays subsequently defined to cross into the second 64K block, generating an error that is not mentioned anywhere handy:

Error code 21!
Variable/memory error
Again, it means you’ve accessed a string that crosses the block boundary on a 256K machine. It brings the program to a screeching halt! Once you know this, the cure is to dimension all the strings first. Uhg. Speed tumbles as the program wades through all the string arrays to find the numerics. The solution we came up with, though a bother, was to dimension just one string array, then store all the various command lists, etc., in it, using an offset variable to find the “base” of each list.

For example, if we had 10 commands, 6 options, and 20 error codes the master string array would look like this:

Cmdlist=0 strings cmdlist+1—cmdlist+10 are commands
Optlist=10 “ optlist+1—optlist+6 “ options
Errlist=16 “ errlist+1—errlist+20 “ error messages

What a classic pain. But, it works.

3: BLOAD+BSAVE+PEEK+POKE = PROGRAM OVERLAY
A heck of an equation. Why resurrect these commands we thought the superior design of the /// made unnecessary? The answer lies in the heart of our program design, and is related to disk space and speed.

The AIDA statistical package consists of a chunk of common code that is always used to process commands, read and save data, etc., plus specific code that is used depending on which command the user gives. The traditional way to handle this is to have a main “menu” program chain various separate programs depending on which command is given. The trouble with this is that the common code has to exist in each chained program. That means longer read time and disk space wasted through duplication of that code.

The ][ version already had solved this by inventing program overlay (see “Call-A.P.P.L.E.,” Nov., 1980). The various command-specific program segments were saved as binary files, then BLOADed quickly right into the middle of the running program, which never knew what had hit it. When we converted to the ///, the improved CHAIN was faster than would have been the case on the ][, but there just wasn’t enough disk space for the program!

Back to the drawingboard. The way overlay worked on the ][ was to find the RAM address of one line of the program: a simple subroutine peeked the Applesoft zero page locations containing the current line pointer — in effect the one-line subroutine found its own address in memory.

Then the code was BLOADed, the only trick being that each “module” so loaded must be shorter than the original “back end” of the program, so as not to overwrite the data.

Brian began to dig around in memory, and added a lot to what we had previously known about reserved locations in BB. Once he found the statement pointer he was able to create the needed “overlay” statements by peeking the counter, then BLOADing the module to that point — after creating PEEK, POKE, BLOAD, and BSAVE, of course!

First, let’s describe those four new commands, which are useful in their own right. Later we’ll combine them into the form needed for program overlay.

Peeking and Poking
Peek and Poke are defined in the AIDA.TOOLS invokable. Both routines are set up to peek or poke one- or two-byte values, and to use the “extend byte” for the RAM banks. The syntax is:

x = exfn%.peek(%address,%xtend,%length)
PERFORM poke(%address,%xtend,@VALUE%,%length)
where: address = address of low byte (0-65535)
xtend = extend byte (0-3)
length = 0 for onebyte, 1 for 2-byte
VALUE% = value to poke

For example:
VALUE%= exfn%.peek(%61,%0,%1)
VALUE%= VALUE% + 512: PERFORM poke(%61,%0,@VALUE%,%1)

The peek will set variable “VALUE%” equal to the two-byte data found at locations 61 (low byte) and 62 (high byte) decimal in the “true” (xtend=0) zero page. The poke will poke the two-byte value contained in “VALUE%” into 61 and 62. This would add 512 bytes onto the BB end of program pointer (clobbering variables, by the way). Careful: poking in particular assumes that you know WHAT you’re poking, and why. The list of zero-page and other BB locations found elsewhere on this disk provides valuable guidance.

Bload and Bsave
The syntax for these commands, set up as invokables, looks like this:
PERFORM bsave(@filename$,%address,%xtend,@savelength%)
PERFORM bload(@filename$,%address,%xtend,@foundlength%)

Before BSAVE the number of bytes to be written must be stored in the “savelentth%” variable. After a BLOAD this same variable will contain the number of bytes actually read from the disk.

All four of these commands are contained in the AIDA.TOOLS invokable described at the end of this article.

Overlay
This technique lets you break long programs into shorter pieces, where you don’t want to have to re-read (as with chain) extensive common code. The program must be laid out like this:

Common code (low line numbers)
Transition subroutine (finds its own RAM address)
Overlay “module” (high line numbers)

When the program is first loaded and run, initialization or startup code is contained in the overlay area. This initialization code MUST be longer than any later module will be, even if you must pad it out with long REM statements. Later this initialization code will be overwritten with the binary modules successively BLOADed.

There are a few other rules for writing the initialization code, based on the obvious fact that those lines of the program aren’t going to be there later. They are: a) no function statements may be defined there, no ONERR or ONEOF statements may be defined or have their destination there,  b) no common or module code may refer to any line number here after the first BLOAD, and c) any string assignments must force the string contents out of the program and into normal string storage. The approach below will do the latter:

READ A$: TITLE$= A$ + “”
B$= “This is a string” + “”

The string concatenation forces BB to move the string out of the program area. Any other string function, such as MID$, could also be used. In our program, the common code runs through line number 997. Line 998 is the transition subroutine, and code just above that point does the actual overlay. Each proto-module is created as a BB program. You may choose to write it following the common code to permit testing.

When ready to save the module as a binary module, SAVE the program first, then EXEC the following text file:

DEL 1, 998: REM Remember, modules can’t have these low line numbers
800 INPUT “MODULE NAME TO SAVE? “;A$: FILE$= “MODULE.”+A$
810 GOSUB 998: last%=exfn%.peek(%61,%0,%1): len%=last% – addr%
820 PERFORM bsave(@FILE$,%addr%,%xtend%,@len%)
830 PRINT FILE$;” contains “;len%;” bytes.”: END
998 addr%=exfn%.peek(%79,%0,%1): xtend%=exfn%.peek(%5712,%0,%0): addr%=addr%+exfn%.peek(%addr+1,%xtend,0): RETURN
RUN

Line 998 calculates the address of the line following it. The peek in line 810 reads the BB end of program pointer. Thus, each module will begin at the line following 998, and the length written is (as calculated in variable “len%”) the number of bytes between this point and the end of the program. In BB it does not matter (as it emphatically does in Applesoft) that the common code be in memory, and always of the same length, when the binary files are written or read. This is because BB uses relative next line pointers. That is, each line of BB code begins with a two-byte pointer containing the number of bytes to add to the line pointer in order to find the next line. In Applesoft this pointer contains the absolute address of the next line. The happy result of this design improvement in BB is that you may modify the common code to your heart’s content after the modules have been saved.

It is a help if the actual execution code in each module begins with the same line number. The common code can then execute that module by a simple GOSUB 1000, or whatever. If more than one command is contained in a module, then you’ll have to keep track of the beginning line numbers of each command, and use an ON k GOSUB ln1,ln2…,ln3 statement. Of course, you also have to know the name of the module in which each command was BSAVEd, and when a command is given by the user, BLOAD that module if it is not now present.

The module code must follow the rules given for the initialization code, above: no embedded strings that are used outside of the module, and no lines or functions called by other modules. The code to load in a module is similar to that above:

800 PRINT”LOADING MODULE “;A$: FILE$= “MODULE.”+A$
810 GOSUB 998
820 PERFORM bload(@FILE$,%addr,%xtend,@len%)
830 RETURN
998 addr%=exfn%.peek(%79,%0,%1): xtend%=exfn%.peek(%5712,%0,%0): addr%=addr%+exfn%.peek(%addr+1,%xtend,0): RETURN

When ready to load a module, GOSUB 800 with A$ equal to the last portion of the module file’s name. Then GOTO or GOSUB to the code in that module. In use, the modules load very quickly, and of course, there is much less space taken up by the program code. Try it.

THE AIDA.TOOLS INVOKABLE The assembler source code for AIDA.TOOLS is found in another file on this disk. The ready-to-use invokable is in its own file elsewhere on this disk. Note that the authors have provided AIDA.TOOLS for the individual use of our readers. Commercial rights are, however, reserved.

/// /// ///

Author Bibliograhies: Dave Lingwood sports the checkered career typical of many in microcomputing: a background in English and Journalism, PhD in Communication Research, and R & D work in technology transfer and marketing, before forming his own software/information service company. He is also Secretary of A.P.P.L.E..

Brian Matthews was one of the first Seattle /// devotees; and he is, as this disk goes to press (drive?), the proud holder of a new Computer Science B.S. from the U of Washington, which he is applying these days for the benefit of Motorola.

Please follow and like us:

About the Author

billm

A.P.P.L.E. Chairman of the Board and Club president — Bill worked for the founder, Val J. Golding and A.P.P.L.E. from 1981 to 1982. In 1999, he began archiving the materials which were distributed and sold by A.P.P.L.E.. That project led to the group that remained of A.P.P.L.E. Bill was involved in the financial industry in Tokyo and has over 20 major office infrastructure projects to his name. In March 2001, he retired to write books and to spend more time pursuing personal interests. As the president of the users group, Bill is in charge of distribution of Call-A.P.P.L.E. magazine as well as the organization of this web site. Bill currently resides in Tokyo, Japan and Shelton, Wa splitting time between the places.