BASIC
TRAINING:
CRASH-PROOF PROGRAMMING
b y C l a y t o n W a l n u m
CRASH-PROOF PROGRAMMING
b y C l a y t o n W a l n u m
Compared to making your programs ready for the user, the actual writing of them is as easy as extinguishing a match with a water hose. Why? Because no matter how hard you try, no matter how many hours you spend trying every conceivable combination of inputs, you will never be sure that you've caught all the bugs or trapped all the possible errors. I sometimes say there's no such thing as a bug-free program. That's a little severe, maybe. It's probably more accurate to say that you'll never be able to prove that your program is bug-free. It might run fine for years, but one day....
So let's take it as a given that we'll never know if our programs are bug-free. That doesn't mean, however, that we don't try to make them bug-free. Once we've got our program running, we must go through it and make sure it can "never" bomb out on the user. This is a big job. Sometimes it takes as much programming to do this as it did to write the program in the first place.
Take, for example, the address-book program we wrote last month. If you recall, that program had little error checking. In fact, I referred to error checking only in passing, promising you that in this month's installment (well, actually, I said "next month," but that was last month, so this month is next month. You see?) we would take that bare-bones program and make it solid enough for anyone to use. This month's Listing 1 is the result.
Notice how much bigger the program is? All that extra code and the program does nothing more than it did last month-except it catches every error I could imagine a user making.
Typing
It In
If you typed in last month's address-book program,
you'll have to type only the new lines this month. Look at Listing 1.
Any line with a line number divisible by ten is from the original
program. All you have to do is add the lines whose line numbers are not
divisible by ten (i.e., Lines 101, 102, 305, 371, 372, etc.).If you didn't type last month's program, you get to type all of Listing 1. Lucky you.
Bomb-Proofing
Once you have the program completely typed, save it
to disk, then run it. The instructions for using the program are in
last month's issue, so I won't repeat them here. This time around I
want you to try and crash the program. With any luck, you won't be able
to. (Pressing Reset or Break is cheating.)Let's take a look at the new code that was added and see how all this error-trapping stuff works.
Lines 101 and 102 we'll refer to later.
Line 305 prints a prompt to the screen whenever the user tries to enter a letter that's not on our menu. In last month's version, we caught the error, but we didn't tell the user what he was doing wrong.
Line 371 calls a subroutine (GOSUB CHECKFILENAME) that checks whether the user entered a legal filename.
What's a subroutine? Many times in our programs, we have to do the same type of operations in different parts of our program. Rather than writing the same code over and over, we write it as a subroutine.
Subroutines work like this: When BASIC sees the keyword GOSUB, program execution jumps to the line number given in the GOSUB statement. Sounds much like a GOTO, doesn't it? Well, it is. The difference is that every subroutine must end with the keyword RETURN, which causes the program execution to go back to the first statement following the GOSUB. In the subroutine call in Line 371, the number of the line to which we want to jump is contained in the variable CHECKFILENAME. We could have written GOSUB 801, but the other way is more descriptive. If you look at Line 102, you'll see where CHECKFILENAME gets its value.
So, in Line 371, we first jump to Line 801 and execute the program statements there, continuing until we find a RETURN, after which we go back to Line 371 and continue with the IF... THEN statement on that line. There are three places in the program where the user enters a filename, and we can use the same subroutine to check them all.
In the subroutine CHECKFILENAME, we set FILENAME$ to an empty string if what the user entered was not acceptable. That's the reason for the IF... THEN statement following the GOSUB. If, after returning from the subroutine, we find that the string is empty, we know to go back and have the user enter a different filename. We'll look in detail at the subroutine itself later.
Because this section of code creates a new file, once the user enters a valid filename, we must check whether that file already exists. Our user won't be very happy if we erase an important file simply because he made a mistake when typing in the filename (or maybe he meant to select the "Load" option rather than "Create").
Line 372 first sets a TRAP. Should we get an error, program execution will jump to Line 379. After setting the TRAP, we try to open for read operations the file the user requested. This is how we can check whether the file already exists, without damaging it if it does. If the file isn't on the disk, we'll get a "file not found" error, and the TRAP statement will send us to Line 379 where we go ahead and open the file for write operations.
Notice that, in this case, we want the error. If we don't get an error, it's because the file the user wants to create already exists (otherwise, we wouldn't have been able to open it). If the file does exist, the TRAP is not activated and we drop down to Line 373.
Line 373 tells the user that the file he's chosen to create already exists and asks if he wants to erase it.
Lines 374-376 retrieve the user's answer and respond accordingly. If the user answers with a "Y" or a "y" (remember: people think of upper- and lowercase letters as equivalent), we go ahead and erase the file with the user's blessing. (If the user made a mistake this time-that is, really wanted to say, "N," that's just too bad. As programmers, we have the right to expect our users to have at least rudimentary intelligence. However, if the results of the "Y" answer could be truly disastrous, it's not a bad idea to give the user one more chance and ask, "Are you sure?")
Line 379 closes the file we opened in Line 372. We can't reuse an open channel; we have to close it first, even if our first OPEN came back with an error.
Line 451 does the same filename checking for the Load function as we did for the Create function. It works exactly the same.
Line 452 sets a TRAP for Line 468. In the Load function, we don't have to worry about erasing the user's file because we're going to open it for append. But we can't open a file that isn't on the disk. So, if when we try to open the file in Line 460, we get an error, the TRAP we've set will send us to Line 468.
Line 468 closes the file, then checks whether the error returned was a "file not found" error. If it was, we tell the user the file doesn't exist and send him back to try again. But wait a minute! What's this PEEK(195) stuff?
Your computer's memory is like a long string of little boxes, each of which will hold one byte of information. These little boxes, just like a row of houses, have "addresses" that we can use to reference them. The first byte in memory is numbered 0, the second byte is 1, the third byte is 2 and so on, all the way through to the last byte. (There are over 65,000 addresses!)
There are many times when we need to look at one of these locations in memory and retrieve what's there. To do that, we need to know its address and then use that address with the PEEK statement.
Many of the locations in your Atari's memory are reserved for special purposes, purposes that never change. For example, location 195 holds the number of the last error that occurred. Hey! What a coincidence! That's the address we just used in our PEEK statement.
The statement PEEK(195) causes BASIC to look into location 195 and tell us what value it finds there. That value will be the number of the last error that occurred. The "file not found" error is number 170. So if, after our TRAP sends us to Line 468, we find that PEEK(195)=170, we'll know that the file we just tried to open didn't exist. Neat, huh? Notice that rather than using the number 170 in our PEEK statement, we've used a variable called FILENOTFOUND, which was initialized in Line 102. By using names like this, rather than cryptic numbers, we'll more easily remember what our code is doing.
If the error we get is not 170, we drop down to Line 469. A "file not found" error is only one of many we could get. It's the most obvious one, so we handle it specifically. Any other error will not be identified by our program. We'll just tell the user that an error occurred and make him try again.
Line 469 tells the user that an error has occurred (an error other than "file not found," which we've already checked for) and goes back to get another filename from him.
Line 681 checks the filename for the View function.
Lines 683, 772 and 775 check to make sure the file the user wants to view exists. If it doesn't, we warn him and make him try again.
Line 801 is the beginning of our subroutine to check the filenames input by the user. In this line, we check that the filename begins with an uppercase letter. Anything else would be illegal. We need the TRAP 809 in case the user just pressed Return without entering a filename.
Notice how we can compare the letters "A" and "Z" as if they were numerical values, checking whether the value in the string is less than or greater than some other value. How do we assign a value to a character? By the order in which they are arranged. Therefore, "A" is less than "Z", and "Z" is greater than "A."
In BASIC we have several operators we can use to compare values:
< Less than > Greater than <= Less than or equals >= Greater than or equals <> Does not equal |
Line 802 checks whether the filename includes the device (D:, D2:, C:, etc.). If the device is there, there has to be a colon in the second or third character of the filename (D: or Dn:, where n is the drive number). If we find the colon in its proper place, we jump to Line 805. The TRAP 808 is there in case the filename is less than three characters long. For example, if the user typed FI as the filename, we will get an error when we try to look at the value of FILENAME$(3,3); there is no character in that position. In this case, we just assume that the device is not present in the filename.
Line 804 adds the device "D:" to the user's filename if it didn't already contain its own device. If this section of the program doesn't make sense to you, go back and review the BASIC Training on string handling (August '89).
Line 805 finds the location of the colon in the filename. Because Atari filenames (and this time I'm referring to the portion of the filename after the device) must begin with a letter from "A" to "Z," we need to know where the first letter is. It is, of course, the first character after the colon.
Line 806 checks whether the first letter of the actual filename is a letter from `A" to "Z." If it is, the filename is okay, and we return from the subroutine. If it's not, we warn the user and set FILENAME$ to an empty string, signalling that the subroutine did not end up with a valid filename.
LET'S
TAKE A LOOK
AT THE NEW CODE
THAT WAS ADDED
AND SEE HOW ALL
THIS ERROR-TRAPPING
STUFF WORKS.
AT THE NEW CODE
THAT WAS ADDED
AND SEE HOW ALL
THIS ERROR-TRAPPING
STUFF WORKS.
Conclusion
We briefly talked about the special purposes some of
your Atari's memory locations have. If you're interested in finding out
more about your computer's memory, you should refer to "The Master
Memory Map," which was published in many parts in ANALOG Computing last
year, or any good 8-bit Atari memory map. However, we will be covering
many of these locations in future BASIC Training columns. Bet you can't
wait.Clayton Walnum is the Executive Editor of ANALOG Computing, as well as the Associate Editor of VIDEOGAMES & COMPUTER ENTERTAINMENT.
LISTING 1
blue indicates inverse video
PM 10 REM ***************************
MI 20 REM * BASIC TRAINING *
AX 30 REM * ADDRESS BOOK 2 *
PZ 40 REM * by Clayton Walnum *
VS 50 REM * *
FG 60 REM * Copyright 1989 *
RT 70 REM * by ANALOG Computing *
PT 80 REM ***************************
BG 90 REM
XR 100 DIM NAME$(30),ADDRESS$(30),CITY$(3
0),PHONE$(15),A$(1),FILENAME$(15)
RA 101 DIM TEMP$(15)
QP 102 CHECKFILENAME=801:STRINGERROR=5:FI
LENOTFOUND=170
QO 110 REM
YA 120 REM ***************************
BC 130 REM * PRINT MENU *
YE 140 REM ***************************
0W 150 REM
BE 160 ? CHR$(125)
JA 170 ? "----------------------"
IE 180 ? "| CREATE FILE |"
EG 190 ? "| |"
QC 200 ? "| LOAD FILE |"
DR 210 ? "| |"
GE 220 ? "| VEIW FILE |"
DU 230 ? "| |"
UP 240 ? "| QUIT |"
JJ 250 ? "----------------------"
ZU 260 INPUT A$
GD 270 IF A$="C" OR AS="c" THEN 370
RE 280 IF A$="L" OR AS="l" THEN 450
TV 290 IF A$="Q" OR AS="q" THEN END
JO 300 IF A$="V" OR AS="v" THEN 680
IP 365 ? "PLEASE CHOOSE C, L, V, OR Q."
01 310 GOTO 260
QS 320 REM
YE 330 REM ***************************
WW 340 REM * CREATE FILE *
YI 350 REM ***************************
RA 360 REM
BR 370 ? "FILENAME";:INPUT FILENAME$
ZF 371 GOSUB CHECKFILENAME:IF FILENAME$="
" THEN 370
ZO 372 TRAP 379:OPEN #1,4,0,FILENAME$
WV 373 CLOSE #1:? :? "FILE ALREADY EXISTS
! ERASE IT (Y/N)";:INPUT A$
LA 374 IF A$="Y" OR A$="y" THEN 380
VO 375 IF A$="N" OR A$="n" THEN 370
GS 376 ? :GOTO 373
MO 379 CLOSE #1
AJ 380 OPEN #1,8,0,FILENAME$
OK 390 GOTO 520
QP 400 REM
YB 410 REM ***************************
SI 420 REM * LOAD FILE *
YF 430 REM ***************************
QX 440 REM
BO 450 ? "FILENAME";:INPUT FILENAME$
XG 451 GOSUB CHECKFILENAME:IF FILENAME$="
" THEN 450
RT 452 TRAP 468
AT 460 OPEN #1,9,0,FILENAME$
OL 462 GOTO 520
DE 468 CLOSE #1:IF PEEK(195)=FILENOTFOUND
THEN ? "NO SUCH FILE! TRY AGAIN.":GOT
O 450
FM 469 ? "UNDETERMINED ERROR! TRY AGAIN."
:GOTO 450
RD 470 REM
YP 480 REM ***************************
UK 490 REM * GET ADDRESS *
YA 500 REM ***************************
Q5 510 REM
VR 520 ? :? "NAME:":INPUT NAME$:IF NAME$=
"" THEN 620
FY 530 ? :? "ADDRESS:":INPUT ADDRESS$:IF
ADDRESS$="" THEN 620
MT 540 ? :? "CITY, STATE & ZIP:":INPUT CI
TY$:IF CITY$="" THEN 620
OX 550 ? :? "PHONE:":INPUT PHONE$:IF PHON
E$="" THEN 620
WI 560 ? :? NAME$:? ADDRESS$:? CITY$:? PH
ONE$
EK 570 ? :? "IS THE ABOVE ENTRY OKAY (Y/N
)";:INPUT A$
FT 580 IF A$="N" OR AS="n" THEN ? :? "PLE
ASE REENTER THE ADDRESS.":GOTO 520
ST 590 IF A$<>"Y" AND AS<>"y" THEN ? :? "
PLEASE ANSWER Y OR N.":GOTO 570
IG 600 ? #1;NAME$:? #1;ADDRESS$:? #1;CITY
$:? #1;PHONE$
NX 610 GOTO 520
BR 620 CLOSE #l:GOTO 160
QX 630 REM
YJ 640 REM ***************************
MO 650 REM * VIEW FILE *
YN 660 REM ***************************
RF 670 REM
BW 680 ? "FILENAME";:INPUT FILENAME$
GN 681 GOSUB CHECKFILENAME:IF FILENAME$="
" THEN 680
QZ 683 TRAP 772
YO 690 OPEN #1,4,0,FILENAME$
OS 700 TRAP 760
UQ 710 COUNT=0:? CHR$(125)
ET 720 INPUT #1;NAME$:INPUT #1;ADDRESS$:I
NPUT #1;CITY$:INPUT #1;PHONE$
TR 730 ? NAME$:? ADDRESS$:? CITY$:? PHONE
$
GN 740 COUNT=COUNT+1:IF COUNT=4 THEN ? :?
"PRESS [RETURN] FOR MORE":INPUT A$:GO
TO 710
DP 750 ? :GOTO 720
WC 760 ? :? "END OF FILE.":? "PRESS [RETU
RN] FOR MENU."
LS 770 INPUT A$:CLOSE #1:GOTO 160
RZ 772 CLOSE #1:IF PEEK(195)=FILENOTFOUND
THEN ? "NO SUCH FILE! TRY AGAIN.":GOT
O 680
NZ 775 ? "UNDETERMINED ERROR! TRY AGAIN."
:GOTO 680
RZ 795 REM
ZM 796 REM ***************************
GZ 797 REM * CHECK FILENAME *
ZS 798 REM ***************************
SL 799 REM
OM 801 TRAP 809:IF FILENAME$(1,1)<"A" OR
FILENAME$(1,1)>"Z" THEN 809
OO 802 TRAP 808:IF FILENAME$(2,2)=":" OR
FILENAME$(3,3)=":" THEN 805
LY 804 TEMP$="D:":TEMP$(3)=FILENAME$:FILE
NAME$=TEMP$
RA 805 X=2:IF FILENAME$(X,X)<>":" THEN X=
3
FM 806 IF FILENAME$(X+1,X+1)>="A" AND FIL
ENAMES(X+1,X+1)<="Z" THEN RETURN
TE 807 GOTO 809
OV 808 IF PEEK(195)=STRINGERROR THEN 804
CE 809 ? "FILENAME ERROR! PLEASE TRY ALAI
N.":? :FILENAME$="":RETURN
ONCE
WE'VE GOT OUR
PROGRAM RUNNING,
WE MUST GO
THROUGH IT AND
MAKE SURE IT CAN
"NEVER" BOMB OUT
ON THE USER.
THIS IS A BIG JOB.
SOMETIMES IT
TAKES AS MUCH
PROGRAMMING TO
DO THIS AS IT DID TO
WRITE THE PROGRAM
IN THE FIRST PLACE.
PROGRAM RUNNING,
WE MUST GO
THROUGH IT AND
MAKE SURE IT CAN
"NEVER" BOMB OUT
ON THE USER.
THIS IS A BIG JOB.
SOMETIMES IT
TAKES AS MUCH
PROGRAMMING TO
DO THIS AS IT DID TO
WRITE THE PROGRAM
IN THE FIRST PLACE.