# Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

# BASIC auto-numbering and labels

Programming on an 8-line screen means barely seeing the current subroutine.

While there are lots of things I dislike about writing in old-school BASIC, number one on the list is probably having to deal with cross-referencing line numbers. It is always annoying to have to add new code, renumber the existing lines, and then make sure that any cross-references have also been fixed.

So, of course, I wrote a Perl script to help me write BASIC programs (Zip file, 3.0 KB).

This allows me to write BASIC programs without line numbers, placing labels where I need them. Some of this is what I vaguely remember from programming in BASIC09 on OS-9 on the Tandy Color Computer.

Here’s a simple program to calculate the day of the week for any date since 1752:

OriginalWith automatic line numbers
• REM This program computes the day of the week
• REM Restriction: The date must be after 1752
• DIM j\$(7)
• LET j\$(1) = "Sunday"
• LET j\$(2) = "Monday"
• LET j\$(3) = "Tuesday"
• LET j\$(4) = "Wednesday"
• LET j\$(5) = "Thursday"
• LET j\$(6) = "Friday"
• LET j\$(7) = "Saturday"
• #LABEL:GETDATE
• PRINT "Month, day, and year (m, d, yyyy)";
• INPUT m,d,y
• IF y > 1752 THEN CALCULATE
• PRINT "YEAR MUST NOT BE PRIOR TO 1753"
• GOTO GETDATE
• #LABEL:CALCULATE
• LET k = INT(0.6+(1/m))
• LET l = y-k
• LET o = m+12*k
• LET p = l/100
• LET z1 = INT(p/4)
• LET z2 = INT(p)
• LET z3 = INT((5*l)/4)
• LET z4 = INT(13*(o+1)/5)
• LET z = z4+z3-z2+z1+d-1
• LET z = z-(7*int(z/7))+1
• PRINT m;"/";d;"/";y;" is a ";j\$(z)
• 10 REM This program computes the day of the week
• 20 REM Restriction: The date must be after 1752
• 30 DIM j\$(7)
• 40 LET j\$(1) = "Sunday"
• 50 LET j\$(2) = "Monday"
• 60 LET j\$(3) = "Tuesday"
• 70 LET j\$(4) = "Wednesday"
• 80 LET j\$(5) = "Thursday"
• 90 LET j\$(6) = "Friday"
• 100 LET j\$(7) = "Saturday"
• 200 REM getdate
• 210 PRINT "Month, day, and year (m, d, yyyy)";
• 220 INPUT m,d,y
• 230 IF y > 1752 THEN 300
• 240 PRINT "YEAR MUST NOT BE PRIOR TO 1753"
• 250 GOTO 200
• 300 REM calculate
• 310 LET k = INT(0.6+(1/m))
• 320 LET l = y-k
• 330 LET o = m+12*k
• 340 LET p = l/100
• 350 LET z1 = INT(p/4)
• 360 LET z2 = INT(p)
• 370 LET z3 = INT((5*l)/4)
• 380 LET z4 = INT(13*(o+1)/5)
• 390 LET z = z4+z3-z2+z1+d-1
• 400 LET z = z-(7*int(z/7))+1
• 410 PRINT m;"/";d;"/";y;" is a ";j\$(z)

The script converted the labels for CALCULATE and for GETDATE into line numbers in the references at lines 140 and 160. It also skipped the second and third sections to line 200 and 300, recognizing that an empty line meant a new section.

Here’s a simpler script with more sections in it, modified from the DWP 230 manual.1

OriginalWith automatic line numbers

[toggle code]

• REM PRESTIGE ELITE 12 (DIABLO P/N 303033-01)
• CLEAR 100
• CSID\$=CHR\$(0)+CHR\$(6*16+7):PAGE\$=CHR\$(1)+CHR\$(11*16+8)
• DIM CL\$(95), SL(95), HL(95)
• REM Switch to IBM mode
• LPRINT CHR\$(7)
• INPUT "Did you hear a beep (y/n)";A\$
• IF A\$ = "y" THEN PRINT "Already in IBM mode." ELSE PRINT "Changing to IBM mode.":LPRINT CHR\$(27);CHR\$(33)
• INPUT "Wheel rotation";RO
• FOR I = 0 TO 95
• IF LEN(C\$) > 1 THEN C\$ = CHR\$(VAL(C\$))
• CL\$(I) = C\$:SL(I) = PS:HL(I) = HS
• PRINT C\$;
• NEXT I
• PRINT
• REM Push data to printer at page PAGE\$
• LPRINT CHR\$(27);"=";CHR\$(12*16+7);CHR\$(0);CHR\$(17);
• LPRINT CSID\$;PAGE\$
• FOR I = 0 TO 95
• IF I+RO <= 95 THEN CHOICE = I+RO ELSE CHOICE = I-(96-RO)
• LPRINT CL\$(CHOICE);
• LPRINT CHR\$(HL(CHOICE)*16+SL(CHOICE)*2);
• NEXT I
• LPRINT CHR\$(32);CHR\$(255)
• LPRINT CHR\$(27);"[T";CHR\$(5);CHR\$(0);
• LPRINT CSID\$;PAGE\$;
• LPRINT CHR\$(2)
• END
• # character, spacing, hammer strength
• #INCREMENT:1
• DATA a, 5, 2
• DATA n, 5, 3
• DATA r, 4, 2
• DATA m, 7, 3
• DATA c, 5, 2
• DATA s, 4, 3
• DATA d, 5, 3
• DATA h, 5, 3
• DATA l, 3, 2
• DATA f, 4, 3
• DATA k, 5, 3
• DATA ",", 3, 1
• DATA V, 6, 3
• DATA "-", 5, 1
• DATA G, 6, 3
• DATA U, 6, 3
• DATA F, 5, 3
• DATA B, 5, 3
• DATA Z, 5, 3
• DATA H, 6, 3
• DATA P, 5, 3
• DATA ")", 3, 2
• DATA R, 6, 3
• DATA L, 5, 3
• DATA S, 5, 3
• DATA N, 6, 3
• DATA C, 6, 3
• DATA T, 5, 3
• DATA D, 6, 3
• DATA E, 5, 3
• DATA I, 3, 2
• DATA A, 6, 3
• DATA J, 4, 3
• DATA O, 6, 3
• DATA "(", 3, 2
• DATA M, 7, 3
• DATA ".", 3, 1
• DATA Y, 6, 3
• DATA ",", 3, 1
• DATA "/", 4, 2
• DATA W, 7, 3
• DATA 9, 5, 3
• DATA K, 6, 3
• DATA 3, 5, 2
• DATA X, 6, 3
• DATA 1, 5, 2
• DATA 2, 5, 3
• DATA 0, 5, 3
• DATA 5, 5, 3
• DATA 4, 5, 3
• DATA 6, 5, 3
• DATA 8, 5, 3
• DATA 7, 5, 2
• DATA "*", 4, 3
• DATA "\$", 5, 3
• DATA "#", 5, 3
• DATA "%", 6, 3
• DATA 60, 5, 3
• DATA "+", 5, 2
• DATA 96, 5, 3
• DATA "@", 7, 3
• DATA Q, 6, 3
• DATA "&", 6, 3
• DATA "]", 4, 2
• DATA "[", 4, 2
• DATA 124, 4, 2
• DATA 92, 5, 2
• DATA 126, 5, 1
• DATA 94, 5, 2
• DATA 123, 5, 2
• DATA 125, 5, 2
• DATA 62, 4, 3
• DATA "!", 3, 2
• DATA "?", 5, 2
• DATA 34, 4, 2
• DATA "'", 2, 1
• DATA "=", 5, 3
• DATA ":", 3, 1
• DATA "_", 6, 2
• DATA ";", 3, 2
• DATA x, 5, 3
• DATA q, 5, 3
• DATA v, 5, 2
• DATA z, 5, 3
• DATA w, 7, 3
• DATA j, 3, 3
• DATA ".", 3, 1
• DATA y, 5, 3
• DATA b, 5, 3
• DATA g, 5, 3
• DATA u, 5, 3
• DATA p, 5, 3
• DATA i, 3, 2
• DATA t, 4, 2
• DATA o, 5, 3
• DATA e, 5, 2

[toggle code]

• 10 REM PRESTIGE ELITE 12 (DIABLO P/N 303033-01)
• 20 CLEAR 100
• 30 CSID\$=CHR\$(0)+CHR\$(6*16+7):PAGE\$=CHR\$(1)+CHR\$(11*16+8)
• 40 DIM CL\$(95), SL(95), HL(95)
• 100 REM Switch to IBM mode
• 110 LPRINT CHR\$(7)
• 120 INPUT "Did you hear a beep (y/n)";A\$
• 130 IF A\$ = "y" THEN PRINT "Already in IBM mode." ELSE PRINT "Changing to IBM mode.":LPRINT CHR\$(27);CHR\$(33)
• 140 INPUT "Wheel rotation";RO
• 200 REM Read character data
• 210 FOR I = 0 TO 95
• 220 READ C\$, PS, HS
• 230 IF LEN(C\$) > 1 THEN C\$ = CHR\$(VAL(C\$))
• 240 CL\$(I) = C\$:SL(I) = PS:HL(I) = HS
• 250 PRINT C\$;
• 260 NEXT I
• 270 PRINT
• 300 REM Push data to printer at page PAGE\$
• 310 LPRINT CHR\$(27);"=";CHR\$(12*16+7);CHR\$(0);CHR\$(17);
• 320 LPRINT CSID\$;PAGE\$
• 330 FOR I = 0 TO 95
• 340 IF I+RO <= 95 THEN CHOICE = I+RO ELSE CHOICE = I-(96-RO)
• 350 LPRINT CL\$(CHOICE);
• 360 LPRINT CHR\$(HL(CHOICE)*16+SL(CHOICE)*2);
• 370 NEXT I
• 380 LPRINT CHR\$(32);CHR\$(255)
• 390 LPRINT CHR\$(27);"[T";CHR\$(5);CHR\$(0);
• 400 LPRINT CSID\$;PAGE\$;
• 410 LPRINT CHR\$(2)
• 420 END
• 999 REM character, spacing, hammer strength
• 1000 DATA a, 5, 2
• 1001 DATA n, 5, 3
• 1002 DATA r, 4, 2
• 1003 DATA m, 7, 3
• 1004 DATA c, 5, 2
• 1005 DATA s, 4, 3
• 1006 DATA d, 5, 3
• 1007 DATA h, 5, 3
• 1008 DATA l, 3, 2
• 1009 DATA f, 4, 3
• 1010 DATA k, 5, 3
• 1011 DATA ",", 3, 1
• 1012 DATA V, 6, 3
• 1013 DATA "-", 5, 1
• 1014 DATA G, 6, 3
• 1015 DATA U, 6, 3
• 1016 DATA F, 5, 3
• 1017 DATA B, 5, 3
• 1018 DATA Z, 5, 3
• 1019 DATA H, 6, 3
• 1020 DATA P, 5, 3
• 1021 DATA ")", 3, 2
• 1022 DATA R, 6, 3
• 1023 DATA L, 5, 3
• 1024 DATA S, 5, 3
• 1025 DATA N, 6, 3
• 1026 DATA C, 6, 3
• 1027 DATA T, 5, 3
• 1028 DATA D, 6, 3
• 1029 DATA E, 5, 3
• 1030 DATA I, 3, 2
• 1031 DATA A, 6, 3
• 1032 DATA J, 4, 3
• 1033 DATA O, 6, 3
• 1034 DATA "(", 3, 2
• 1035 DATA M, 7, 3
• 1036 DATA ".", 3, 1
• 1037 DATA Y, 6, 3
• 1038 DATA ",", 3, 1
• 1039 DATA "/", 4, 2
• 1040 DATA W, 7, 3
• 1041 DATA 9, 5, 3
• 1042 DATA K, 6, 3
• 1043 DATA 3, 5, 2
• 1044 DATA X, 6, 3
• 1045 DATA 1, 5, 2
• 1046 DATA 2, 5, 3
• 1047 DATA 0, 5, 3
• 1048 DATA 5, 5, 3
• 1049 DATA 4, 5, 3
• 1050 DATA 6, 5, 3
• 1051 DATA 8, 5, 3
• 1052 DATA 7, 5, 2
• 1053 DATA "*", 4, 3
• 1054 DATA "\$", 5, 3
• 1055 DATA "#", 5, 3
• 1056 DATA "%", 6, 3
• 1057 DATA 60, 5, 3
• 1058 DATA "+", 5, 2
• 1059 DATA 96, 5, 3
• 1060 DATA "@", 7, 3
• 1061 DATA Q, 6, 3
• 1062 DATA "&", 6, 3
• 1063 DATA "]", 4, 2
• 1064 DATA "[", 4, 2
• 1065 DATA 124, 4, 2
• 1066 DATA 92, 5, 2
• 1067 DATA 126, 5, 1
• 1068 DATA 94, 5, 2
• 1069 DATA 123, 5, 2
• 1070 DATA 125, 5, 2
• 1071 DATA 62, 4, 3
• 1072 DATA "!", 3, 2
• 1073 DATA "?", 5, 2
• 1074 DATA 34, 4, 2
• 1075 DATA "'", 2, 1
• 1076 DATA "=", 5, 3
• 1077 DATA ":", 3, 1
• 1078 DATA "_", 6, 2
• 1079 DATA ";", 3, 2
• 1080 DATA x, 5, 3
• 1081 DATA q, 5, 3
• 1082 DATA v, 5, 2
• 1083 DATA z, 5, 3
• 1084 DATA w, 7, 3
• 1085 DATA j, 3, 3
• 1086 DATA ".", 3, 1
• 1087 DATA y, 5, 3
• 1088 DATA b, 5, 3
• 1089 DATA g, 5, 3
• 1090 DATA u, 5, 3
• 1091 DATA p, 5, 3
• 1092 DATA i, 3, 2
• 1093 DATA t, 4, 2
• 1094 DATA o, 5, 3
• 1095 DATA e, 5, 2

You can see that it moved the blank-line sections up to the nearest hundred, and the pound-sign section up to the nearest thousand. It also incremented the data statements by 1 instead of the default 10. In this case that’s useful because it helps me ensure I have the correct number of DATA statements. Each DATA statement corresponds to a daisy wheel position, and there are 96 of them (0 to 95, in the numbering of this BASIC program).

Also helping to ensure correct data lines, the #DATA directive takes all succeeding lines and puts them into data statements whose size (unless one element is greater than about 30 characters long) does not exceed 40 characters per line. #ENDDATA ends the data collection, potentially with a final data statement, such as “DATA end”. This makes it easy to keep DATA elements in order, such as alphabetical order, using sorting services on macOS.

OriginalWith automatic line numbers
• #DATA feelings
• despair
• enchantment
• grief
• guilt
• hate
• joy
• love
• pain
• passion
• pride
• remorse
• sorrow
• #ENDDATA end
• 2999 REM feelings
• 3000 DATA despair, dread, enchantment
• 3010 DATA grief, guilt, hate, joy, love
• 3020 DATA pain, passion, pride, remorse
• 3040 DATA end

DirectiveResult
#LABEL:text [remark]store this label for use in GOTO, GOSUB, and THEN statements, optionally using extended text as a REM statement
blank lineround next line up to the nearest hundred
# textround next line up to the nearest thousand, and use text as a REM statement
## textround next line up to the nearest 500th, and use text as a REM statement
#INCREMENT:numberincrement succeeding lines by this amount
#DATA [remark]slurp the next lines (until end or #ENDDATA) into 40-character DATA statements
#ENDDATA [final datum]end data slurping, optionally with a final data line

There is a default label of THISLINE that just goes back to the current line. For example, in While sorrowful dogs brood: The TRS-80 Model 100 Poet the program reads the data in to count how many nouns, feelings, verbs, and adjectives/adverbs it has:

OriginalWith automatic line numbers
• REM Inspired by Jay Chidsey, Bit Smitten Part V, 80 Micro, December 1982 (#35) p. 101
• REM and Margaret Chisman, Producing Computer Poetry, The Best of Creative Computing Volume 2 p. 106
• REM SETUP
• CLS:CLEAR 1500
• R1 = VAL(RIGHT\$(TIME\$, 1)) 'Tens digit for random seed
• VOWEL\$ = "aeiou"
• CR\$ = CHR\$(13) + CHR\$(10):PR\$ = "." + CR\$
• REM Count words
• READ N\$:N=N+1:IF N\$<>"end" THEN THISLINE
• READ F\$:F=F+1:IF F\$<>"end" THEN THISLINE
• READ V\$:V=V+1:IF V\$<>"end" THEN THISLINE
• READ A\$:A=A+1:IF A\$<>"end" THEN THISLINE
• 10 REM Inspired by Jay Chidsey, Bit Smitten Part V, 80 Micro, December 1982 (#35) p. 101
• 20 REM and Margaret Chisman, Producing Computer Poetry, The Best of Creative Computing Volume 2 p. 106
• 30 REM SETUP
• 40 CLS:CLEAR 1500
• 50 R1 = VAL(RIGHT\$(TIME\$, 1)) 'Tens digit for random seed
• 60 VOWEL\$ = "aeiou"
• 70 CR\$ = CHR\$(13) + CHR\$(10):PR\$ = "." + CR\$
• 100 REM Count words
• 110 READ N\$:N=N+1:IF N\$<>"end" THEN 110
• 120 READ F\$:F=F+1:IF F\$<>"end" THEN 120
• 130 READ V\$:V=V+1:IF V\$<>"end" THEN 130
• 140 READ A\$:A=A+1:IF A\$<>"end" THEN 140

When matching cross-references, it makes a half-hearted attempt to find misspellings: if anything other than a number follows a GOTO or GOSUB after the line has had its cross-references replaced, the script will abort with an error message. This does not catch errors after THEN, after ELSE, or after commas (such as in an ON GOTO or ON GOSUB statement). Also, if your BASIC allows storing line numbers in variables, you’ll probably want to remove this check.

The script also currently only replaces cross-references following GOTO, GOSUB, THEN, ELSE, as well as commas following a GOTO or GOSUB (for ON x GOTO/GOSUB…). As I use the script more often, I’ll probably find more places where line numbers are allowed and thus where I need to catch and replace labels.

[toggle code]

• #!/usr/bin/perl
• #convert source file into BASIC program lines with line numbers
• use POSIX;
• \$return = "\r\n";
• #width of Model 100 display
• #currently used only for automatic DATA lines
• \$screenWidth = 40;
• if (\$ARGV[0] =~ /^--return\$/ && \$ARGV[1] =~ /^(cr|lf|crlf)\$/) {
• shift @ARGV;
• \$return = shift @ARGV;
• if (\$return eq 'cr') {
• \$return = "\r";
• } elsif (\$return eq 'lf') {
• \$return = "\n";
• } elsif (\$return eq 'crlf') {
• \$return = "\r\n";
• }
• }
• #compile DATA lines
• sub compileData {
• return if \$#data == -1;
• local(\$dataLine) = '';
• for \$data (@data) {
• if (length(\$dataLine) + length(\$data) > (\$screenWidth-(6+length(\$line)))) {
• makeDataLine();
• }
• \$dataLine .= ', ' if \$dataLine;
• \$dataLine .= \$data;
• }
• makeDataLine() if \$dataLine;
• undef(@data);
• }
• sub makeDataLine {
• \$lines[\$#lines+1] = "\$line DATA \$dataLine";
• \$dataLine = '';
• \$line += \$increment;
• }
• sub sectionBreak {
• my(\$break, \$remark) = @_;
• \$lines[\$#lines+1] = "" if \$lines[\$#lines] ne "";
• \$line = ceil(\$line/\$break)*\$break;
• if (\$remark) {
• \$line -= 1 if \$increment >= 5;
• \$lines[\$#lines+1] = "\$line REM \$remark";
• \$line += 1 if \$increment >= 5;
• }
• }
• \$line = 10;
• \$increment = 10;
• while (<>) {
• chomp;
• if (m/^\$/) {
• sectionBreak(100);
• } elsif (m/^#INCREMENT:([0-9]+)\$/) {
• \$increment = \$1;
• } elsif (m/^#ENDDATA( (.*))?\$/) {
• \$finalDatum = \$2;
• if (!\$inData) {
• print STDERR "#ENDDATA without #DATA\n";
• die;
• }
• \$inData = 0;
• compileData();
• if (\$finalDatum) {
• \$lines[\$#lines+1] = "\$line DATA \$finalDatum";
• \$line += \$increment;
• }
• } elsif (\$inData) {
• \$data[\$#data+1] = \$_;
• } elsif (m/^#LABEL:([a-zA-Z][a-zA-Z0-9_]+)(.*)/) {
• \$labels{\$1} = \$line;
• if (\$2) {
• \$remark = \$2;
• \$remark =~ s/^\s+|\s+\$//g;
• } else {
• \$remark = lc(\$1);
• \$remark =~ s/_/ /g;
• }
• \$lines[\$#lines+1] = "\$line REM \$remark";
• \$line += \$increment;
• } elsif (m/^##? (.+)\$/) {
• \$remark = \$1;
• \$break = 1000;
• \$break = 500 if m/^## /;
• sectionBreak(\$break, \$remark);
• } elsif (m/^#DATA (.*)\$/) {
• if (\$remark = \$1) {
• sectionBreak(1000, \$remark)
• }
• \$inData = 1;
• } else {
• \$lines[\$#lines+1] = "\$line \$_";
• \$line += \$increment;
• }
• }
• compileData() if \$inData;
• foreach \$line (@lines) {
• \$line =~ m/^([1-9][0-9]*) /;
• \$labels{'THISLINE'} = \$1;
• \$labelNames = join('\b|', keys %labels) . '\b';
• #replace simple gosubs, gotos
• \$line =~ s/(GOSUB|GOTO|THEN|ELSE)( ?)(\$labelNames)/\$1\$2\$labels{\$3}/g;
• #replace ON X GOSUB, ON X GOTO
• while (\$line =~ s/(GOSUB|GOTO)([ ,0-9]*)(\$labelNames)/\$1\$2\$labels{\$3}/g){}
• #If there remain non-numeric characters after a GOSUB or a GOTO, that's an error on the Model 100/200
• if (\$line =~ m/(GOSUB|GOTO)[ ]*([^0-9 :]+)/) {
• print STDERR "NON-EXISTENT LABEL \$2 ON LINE \$line\n";
• print STDERR join(', ', keys %labels), "\n";
• die;
• }
• print "\$line\$return";
• }

As you can see, the script is a simple one. It loops through each line looking for directives and saving up each line, giving each line its number. Then it loops again, looking for cross-references and printing each line after hopefully replacing all cross-references.

There is also a command-line switch, --return, for changing the line endings between the default CRLF to either CR or LF. If the old-school computer you’re using this on has a different line ending, you’ll probably just want to change the default at the top of the script.

Getting rid of line numbers in the source files makes it a lot easier to use version control on my BASIC programs. Before, any change in line number would trigger a change detection, even if the only thing that changed was the line number, in order to make room for something else. This tended to obscure the real code change, making version control much less useful for old-school BASIC programs. By using this script, only actual changes in code are displayed in Mercurial2, making the real change much easier to see.

Another advantage to this script is that it accepts multiple files on the command line. For the daisy wheel data selector I’m working on, I have one BASIC file with the code, and several with wheel data from the manual, as well as one I’m trying to get working. To generate the BASIC program for a specific wheel, I specify the code file and then the data file.

• ~/bin/basicize ibmwheel.bas diablo.bas > ~/Model100/diablo.ba

While writing BASIC programs in a text editor on a non-BASIC workstation means foregoing the interactivity, it is still a lot easier to write programs in full-screen text editors such as Textastic. And with modern drag and drop and emulators, it’s still very easy to test out many of the BASIC programs I’m writing for old-school computers, using emulators such as VirtualT.

1. Note that it doesn’t actually work yet, as far as I can tell. It doesn’t generate any errors, but it also does not behave as expected.

2. I use SourceTree on the Mac.