Merging BASIC Programs From Commodore Disk
Jim Butterfield, Associate Editor
It's often very useful to be able to merge two programs. Here, Jim Butterfield steps through a disk merger program that is helpful also in understanding how programs are formatted and stored on disk. For all Commodore machines.
Programs can be merged using a curious technique with cassette tape – see "BASIC Program Merges: PET And VIC," COMPUTE!, June 1982, page 158. We can also do a disk merge in a much more straightforward manner.
With disk, we can have several files going at the same time. Thus, we can read two separate programs and write out the new combination program as a single activity.
With a disk unit, we can read in programs as if they were sequential files. This means that we can manipulate a program as if it were data; for that matter, we can write a data file which may be later used as a program. This opens the door to sophisticated activities, such as programs that analyze other programs, or programs that write programs.
The Merge Program
To show how it's done, and to provide a useful capability, we'll walk through some simple programming which will merge two programs. This MERGER program will work on all Commodore disk-based systems: VIC-20, Commodore 64, and PET/CBM. It's written in BASIC to enable you to see how everything works.
As we walk through the program lines, we'll point out special considerations that we need to take into account when reading and writing program files (as opposed to data files).
First, we identify the program:
100 PRINT "PROGRAM MERGER"
Each of the two input files will have individual working values. For example, C$ is the line of code we are working on; N is the line number. Let's make room:
110 DIM A$(2), B$(2), C$(2), N(2)
Let's open the error channel so we can spot problems:
120 OPEN 15, 8, 15
Now we'll ask for the name of the first program to be merged. We'll OPEN the file as a program (note the, P for Program), then we'll check for problems and quit if we see trouble:
130 INPUT "PROGRAM 1" ; X$ 140 OPEN 1, 8, 2, X$ + ", P, R" 150 INPUT # 15, E, E$, E1, E2 160 IF E THEN PRINT E$: CLOSE 15 : END
One extra thing to do here. The first two bytes of a program file contain the program's start address. We will assume that we won't need this information, since VIC and 64 programs will relocate automatically, and PET/CBM programs must start at address 1025. Finally, we'll grab the first line of the program by calling subroutine 420 (more on this when we reach it).
170 GET # 1, A$, A$ : J = l : GOSUB 420
We're ready to repeat the sequence for program two. In this case, we'll allow the user to reply "N" in order to specify that no program two exists. In such a case, we'll merge program one with nothing, and just copy program one. But there's a bonus which we'll discuss later.
180 INPUT "PROGRAM 2 (OR 'N')" ; X$ 190 IF X$ = "N" THEN N(2) = 1E9 : GOTO240 200 OPEN 2, 8, 3, X$+",P, R" 210 INPUT#15, E, E$, E1, E2 220 IF E THEN PRINT E$: CLOSE 15: END 230 GET#2, A$, A$ : J = 2 : GOSUB 420
Now we'll ask for the name of the new, merged file that we are about to create. We OPEN with ".., P, W"; in other words, program file in write mode.
240 INPUT "NEW FILE" ; X$ 250 OPEN 8, 8, 8, "0:" + X$ + ", P, W" 260 INPUT # 15, E, E$, E1, E2 270 IF E THEN PRINT E$ : CLOSE 15 : END
You may recall that we threw away the start addresses from our program input files. Before we start to write our output file, we must insert a start address so that the format is correct. VIC and 64 won't care, but PET/CBM needs an address of 1025, so that's what we'll supply in two bytes:
280 PRINT#8,CHR$(1) ; CHR$(4); 290 REM COMPARE LINES HERE
The above REMarks line says it all. We have a line from each program. The line numbers are held in N(l) and N(2). We wish to take the lowest line number; if they are the same, we'll take from file two:
300 X = 2 310 IF N(1)<N(2) THEN X = l
When we reach the end of a file, we make the line number impossibly large. If both line numbers are way up there, we are finished and can wrap up our output file:
320 N = N(X) : IF N>1E8 GOTO 380
Now we're ready to print the BASIC line from file X (X is 1 or 2). We'll need to know the format in more detail. The first two bytes of each line are called a "forward link." We don't need to work them out precisely: provided they are not zero bytes, they will be rebuilt when the program is loaded into the computer. So we can print a couple of CHR$(l)'s.
The next two bytes are the line number, in binary. We happen to have this information in bytes A$(X) and B$(X) – we obtained this in the subroutine at 420 – and we can just print them.
The line of BASIC follows. That's in C$(X); and the line must be followed by a binary zero, or CHR$(0), to terminate it properly. So the whole line goes:
330 PRINT#8, CHR$(1);CHR$(1);A$(X);B$(X);C $(X);CHR$(0);
Now we want to replace the line we've just used. We could read from file X with a call to 420 – but wait a moment. If both line numbers are the same, we want to replace them both. That's what a merge is about: one line overwriting another when the numbers match.
340 FOR J = l TO 2 350 IF N(J) = N THEN GOSUB 420 360 NEXT J
We've handled the line in question. Let's go back and do some more.
370 GOTO 300
If we've reached the end–no more input lines–we terminate the output file with two zero bytes (a "null" forward link) and close down.
380 PRINT#8, CHR$(0) ; CHR$(0); 390 CLOSE 1 : CLOSE 2 : CLOSE 8 : CLOSE 15 400 END 410 REM READ LINE OF BASIC
Here comes our subroutine to read from file number J (J may be 1 or 2). First, we grab the first two bytes (the "forward link"). We don't need these, except for one thing: if they are both zero bytes, we are at the end of the BASIC program.
Zero bytes arrive in an odd way. You'd think that the GET statement would receive the equivalent of CHR$(0), a single character containing zero bits. Nope. Due to an eccentricity of BASIC, they arrive as a "null string"; no bytes at all. So that's what we must test for:
420 GET#J, A$, B$ 430 IF A$ = "" AND B$ = "" GOTO 540
Now we go for the line number, which we read into A$(J) and B$(J). If they happen to be null strings, we correct them to CHR$(0). Once we've done that, we can calculate the value of the line number and put it into N(J):
440 GET#J, A$(J), B$(J) 450 IF A$(J) = "" THEN A$ (J) = CHR$(0) 460 IF B$(J) = "" THEN B$(J) = CHR$(0) 470 N(J) = ASC(A$(J)) + ASC(B$(J))*256
We've arrived at the BASIC line text itself. Let's gather it into a string called C$(J). As we collect the bytes, we must watch for the zero byte (or null string, to us) that flags end-of-line:
480 C$(J) = "" 490 GET #J, A$ 500 IF A$ = "" GOTO 550 510 C$(J) = C$(J) + A$ 520 IF ST = 0 AND LEN(C?(J))<254 GOTO 490
The above line checks for anomalies. If ST is not zero, we've reached the end of file, or we're having trouble with the disk interface. Either way, we want to warn the user or quit. And if C$(J) is getting too long, we must be into something that isn't really a BASIC program. In either case, we'll drop into a warning statement:
530 PRINT "PROBLEM FILE";J
If we see the "forward link" of two zeros that flags end-of-BASIC, we set the line number to a ridiculously high value:
540 N(J) = 1E9
And in any case, we return to the calling point:
When you type in this program, be sure that lines 330 and 380 end with a semicolon. If you miss this, you won't get a program; you'll get a mess.
Beginning programmers may not have seen statements such as GET#J,..., where a variable selects which file will be used. A little thought will reveal how it works and will possibly open up new trains of thought on the effective use of BASIC.
Programs produced by MERGER will load into any Commodore machine. As mentioned before, VIC-20 and Commodore 64 (and the new B series) will automatically relocate programs to the proper address. For the PET and CBM, we have supplied the start address needed by these somewhat less flexible loaders.
This means that a simple run of MERGER with one input program file (replying "N" to the second file name) will convert a program into PET-loadable form.
This is not a wholesale conversion program, of course. A program may be loadable to another machine, but still won't run because the POKEs and PEEKs are incompatible.
You might like to keep your favorite subroutines as small programs on disk, and merge them into other programs as needed. Be sure to keep your line numbers within compatible ranges so that the new program lines don't overwrite needed parts of a program.
Many calculation programs run using DATA statements. If these statements are saved as a program file, they can be merged into the calculation programs as needed, saving a great deal of retyping.
Perhaps most important of all: a careful reading of this program will reveal a good deal about how programs are formatted and stored on disk.