image of READY prompt

Wang2200.org

The Wang 2200 was a pretty rock solid machine, especially compared to some of the micros that followed it in the mid- to late 70's. However, that doesn't mean that the Wang computer and its particular peculiar dialect of BASIC were perfect either.

Here is a short collection of outright bugs and some surprising artifacts of Wang BASIC. Unless otherwise noted, all are from Wang BASIC as implemented on the 2200T CPU; not all of them will behave identically with other microcode variations.

In particular, Wang BASIC-2 suffers none of the bugs here (just some of the unusual dialect issues), and was an amazingly robust interpreter. That was due, in part, to higher expectations and coding practicies of the Wang engineers, and that the VP had a writable microcode store, enabling frequent updates and bug fixes.

In all cases below, lines that begin with ":" indicate the lines which the user has typed; the other lines are the computer's response.

#1 – Make the Interpreter Spew Trash (link)

bug image

This trick comes courtesy of Carl Coffman. Wang BASIC, like most interpreted BASICs, has both program mode and immediate mode execution. Immediate mode execution means that a single line program can be entered and executed immediately without being part of a stored program. Some examples are as follows:

:PRINT SQR(2)
 1.4142135624
:A=1:PRINT A+1
 2
:FOR I=1 TO 3:PRINT I, I*I:NEXT I
 1     1
 2     4
 3     9

Although branching is not allowed as it makes no sense for a one line program, the FOR/NEXT construct is allowed. However, the Wang BASIC interpreter didn't count on sneaky folks performing a FOR/NEXT loop as two separate immediate commands. Type the following two commands into a real Wang 2200 (or use the emulator) and watch the interpreter spew out some junk to the screen until it comes back to its senses. Miraculously, the program that was in memory appears unharmed. That is simply a bug.

:FOR I=1 TO 10
:NEXT I

Interestingly, this fails only for this case. Try this program -- it works.

:FOR I=1 TO 3:PRINT I
 1
 
:NEXT I
 2
 
:NEXT I
 3
 
:NEXT I
 
:NEXT I
 
 NEXT I
       ^ERR 26
:_

This can go even one further. The immediate mode NEXT I can branch back into a suspended program.

:CLEAR
READY
:10 FOR I=1 TO 3
:20 PRINT I
:30 STOP
:40 PRINT "Here"
:50 NEXT I
:60 PRINT "Done"
:RUN
 1
 
STOP
:NEXT I
 2
 
:NEXT I
 3
 
:NEXT I
 
:_

Note that the string "Here" isn't printed, meaning that the NEXT I really does loop right back to line 20. However, when the terminal count is reached, it drops back into immediate mode instead of continuing on at line 60.

One final "FOR loop funny" is that FOR loops with the same index can be nested, although the index variable gets modified by the inner loop and the outer loop abides by it.

:CLEAR
READY
:10 FOR I=1 TO 30 STEP 10
:20 FOR I=I TO I+2
:30 PRINT I,
:40 NEXT I
:50 PRINT
:60 NEXT I
:RUN
 1               2               3
 13              14              15
 25              26              27
 
:_

What this shows is that the FOR loop control stack doesn't check for nesting on the same index.

Also see Stupid Trick #21.

#2 – Code Entry for People with Poor Peripheral Vision (link)

This isn't a bug, just misapplication of a feature. Wang BASIC allowed specifying the width of a terminal or printer since various options existed, so BASIC had no way of knowing a priori what the correct width was. The SELECT statement was used for this purpose. SELECT could be applied to a specific device, and it could be applied in specific situations, such as console output, listing, or printing.

In immediate mode type this command: SELECT CI 005(1). Then start typing in your program. All output appears in the first column, since the above command claimed that the CRT had only one column.

#3 – "Marked GOTO" (link)

Wang BASIC has a feature called a "marked GOSUB". Rather than using a line number as the target of the GOSUB command, a numeric label, from 0 to 255, is used instead. An example would help.

:CLEAR
READY
:10 PRINT "Starting"
:20 GOSUB'5
:30 END
:100 DEFFN'5
:110 PRINT "Hello"
:120 RETURN
:RUN
Starting
Hello
 
:_

OK. However, this can be abused by using the RETURN CLEAR command. The RETURN CLEAR command pops the return address off of the call stack. It can be useful for aborting a deeply nested subroutine when some error occurs. By combining this command with the marked GOSUB, we make a "marked GOTO".

:CLEAR
READY
:10 PRINT I
:20 IF I<4 THEN 30:GOSUB'3
:30 I=I+1:GOTO 10
:40 PRINT "This should never happen":END
:80 DEFFN'3
:90 RETURN CLEAR: REM we never intend to return
:110 I=I+2
:120 GOTO 10:REM we never run out of stack space

#4 – Code in a REM (link)

This isn't a bug at all; Wang documented it. However, Wang BASIC is the only BASIC I've seen that can terminate a REM with a statement separator, and it is surprising.

:CLEAR
READY
:10 REM This is a comment:PRINT "Hello"
:RUN
Hello
 
:_

#5 – FORM=STOP (link)

Surprisingly, the following is a legal Wang BASIC fragment:

:100 FORM=STOP

This is legal despite the fact that Wang variables can have only a single letter or a single letter plus one digit, and despite the fact that the keyword STOP is used. This is because the code isn't an assignment at all, but gets parsed by the interpreter as a FOR statement.

:100 FOR M = S TO P

Yes, it isn't much, but back in 1978 I was excited when I figured this one out.

#6 – Comma Chameleon (link)

Wang's PRINTUSING allowed a number of formatting options, including inserting commas into numeric fields. It didn't require that you had to follow conventions, though.

:CLEAR
READY
:10 PRINT USING 20, 123456789
:20 % #,#,#,#,#,#,#,#,#
:RUN
 1,2,3,4,5,6,7,8,9
 
:_

Commas aren't allowed after the decimal point, however, and are assumed to separate the next image specifier.

#7 – Microcode Abuse (link)

The $GIO instruction allows executing synthetic microcode programs for performing fast I/O to peripherals, such as serial ports and reel-to-reel tape drives. However, they keyboard and display are just peripherals as far as the microcode is concerned. With some cunning, I'm sure some interesting feats could be achieved, such as high speed screen drawing, but I must admit to just speculating here. Some rainy day I'll make an attempt and report my results here.

#8 – PACK/UNPACK Numeric Scaling (link)

The PACK and UNPACK commands allow encoding a number or an array of numbers into a string array, using whatever precision is desired. Part of the PACK operation is a format specifier saying whether to encode the number as fixed or float, and how many digits of precision to keep. UNPACK does the reverse, but since the format of the PACK operation isn't encoded in the packed data, the format must also be specified by the UNPACK instruction to recover the data.

Where the abuse comes in is that it is possible to specify different formats when PACKing and UNPACKing. One trick would be to scale an array of numbers by a power of 10 like this:

10 DIM A$(100)10:REM 1000 bytes
20 DIM B(125), C(125):REM 125 numbers each
30 FOR I=1 TO 125:B(I)=RND(1):NEXT I
40 PACK (+#.###########) A$() FROM B()
50 UNPACK (+##.##########) A$() TO C()

This program sets up an array of 125 random numbers in B(), then scales them by a factor of 10 and puts them in C().

I timed this and it is about 50% faster than using the scalar MAT multiply function

...
40 MAT C=(10)*B

However, for this trick to work, one must know the magnitude of the numbers being operated on otherwise severe truncation could result. But that just leads to the next idea: if you want to truncate an array of numbers to some number of digits (say, two), this mechanism would be many times faster than looping over the array and doing something like:

100 FOR I=1 TO 125
110 C(I) = INT(B(I)*100)/100
120 NEXT I

#9 – Exposing a Terminal Disease (link)

bug image

This one is a real bug. Not that interesting, but a bug anyway.

The bug isn't in the terminal, which is simply a video display driven from the CRT controller in the CPU. It isn't a CRT controller bug, since every time a character is sent to the controller, the cursor is automatically incremented and is supposed to just wrap modulo the screen width. It is up to the BASIC microcode to know when to insert carriage returns and line feeds. This is especially true in light of the earlier SELECT 005(<width>) discussion.

The CRT interprets some bytes with ASCII values less than 32 as control codes. Some of those control codes are just ignored, some cause a carriage return, a line feed, clearing the screen, or cursor motions. HEX(08) is the code for cursor left, and HEX(09) is the code for cursor right. Normally the cursor gets bumped to the next line of the display when a character is emitted in the last column of the display, but if these two codes were used before that, the CR/LF is emitted at the wrong time.

:CLEAR
READY
:10 PRINT HEX(03);:REM clear the screen
:20 PRINT HEX(0909090909090909);:REM move the cursor right 8 spaces
:30 PRINT "!";
:40 FOR I=1 TO 62:PRINT "a";:NEXT I
:50 FOR I=1 TO 8:PRINT "b";:NEXT I
:RUN
aaaaaaab!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbb

The "!" came out first. Note that the a's wrap around on the same line until 64 characters total have been output, then an implicit CR/LF is issued, causing the final 7 b's to appear on the second line.

Explanation of Bug

The code in the BASIC interpreter that figures out where the cursor is on the display doesn't account for the horizontal cursor motions properly. Apparently is knew that HEX(03) caused the cursor to get reset, but it didn't account for the HEX(09)'s adjusting the cursor's horizontal offset.

#10 – Input/Output = Output/Input (link)

bug image

When someone walks away from their Wang for a minute, type this as an immediate mode command: SELECT PRINT 001. When the user comes back to the computer, he will be able to list and edit the program as normal. However, when he runs his program, nothing shows up. This is because this command has redirected the output of PRINT statements to the keyboard! The keyboard, of course, can't use the data, but it does happen to return the right status so that BASIC thinks the data has been printed and doesn't lock up the machine.

Typing SELECT PRINT 005 puts everything back right.

Similarly, one could enter SELECT LIST 001. Everything operates as before, except the LIST command mysteriously doesn't work when it is invoked.

Another one is: PRINT HEX(03);":";:SELECT CO 001. In this case, PRINTing and LISTing work OK, but none of the keystrokes of the user and error messages appear. The gibberish at the beginning clears the screen and prints a fake prompt so that the evidence of what just happened isn't on screen.

More maliciously, wait until a user has been entering their program for 15 minutes without saving their work and type in SELECT CI 005.

This tells the computer to take console input from the CRT (005) instead of the keyboard (001). Since the CRT can't return the proper status, they machine locks up waiting for input that will never come. A RESET will clear the screen, but it is still left with console input selected from the CRT. The only way out of it is to power cycle.

#11 – Inert Code (link)

More an oddity than anything, this "trick" exposes a strange feature of Wang BASIC. The BASIC manual mentions that the PRINTUSING image statement can't have any literal '#' in it for obvious reasons, but it also says it can't have any colons either.

The reason for that restriction is that the entire line is parsed, and anything after a colon must be a legal statement. Despite that requirement, those statements after the colon are never executed.

:CLEAR
READY
:10 PRINT USING 20, 123
:20 %This is a test ###:PRINT "HI"
:30 PRINT "Done"
:RUN
This is a test 123
Done
 
:_

#12 – Copy Machine (link)

Someone who wrote this code might be surprised if they expected the final value of A$ to be "aabcdefg...". The code appears to set A$ to the alphabet, then copy bytes 1 through 25 of the string to bytes 2 through 26.

:CLEAR
READY
:10 DIM A$26
:20 A$="abcdefghijklmnopqrstuvwxyz"
:30 PRINT A$
:40 STR(A$,2,25)=STR(A$,1,25)
:50 PRINT A$
:RUN
abcdefghijklmnopqrstuvwxyz
aaaaaaaaaaaaaaaaaaaaaaaaaa
 
:_

Explanation of Bug

What has happened is that the BASIC interpreter copies the first byte of the string to the location of the second byte. Then the second byte is copied to the location of the third byte, etc. Notice that each time the "a" is copied to the next position. Just about anybody who has written a block move routine will have made this mistake once. If the range is overlapping, an in-place copy routine like this must copy from the end of the string to the beginning otherwise this behavior results.

This could be turned into a feature. While the above code could be more directly accomplished with this:

40 INIT("a") A$

The INIT statement can deal only with a single fill byte. If you want to fill with a pattern, do this:

:CLEAR
READY
:10 DIM A$33
:20 A$="abc"
:30 STR(A$,4,30)=STR(A$,1,30)
:40 PRINT A$
:RUN
abcabcabcabcabcabcabcabcabcabcabc
 
:_

#13 – Spacing Out (link)

The Wang interpreter tokenizes its input (that is, PRINT becomes a single byte in memory). However, it is conservative about spaces, even spaces that appear before the line number. It can be used for indenting, or making program lines line up. For example:

READY
:CLEAR
READY
:10 REM Normal practice of putting in limited spacing
:50 FOR A=1 TO 10
:100 PRINT "A=";A
:150 PRINT "A*A=";A*A
:200 NEXT A
:250 END
:LIST
10 REM Normal practice of putting in limited spacing
50 FOR A=1 TO 10
100 PRINT "A=";A
150 PRINT "A*A=";A*A
200 NEXT A
250 END
:_

By using some spacing, things look nicer:

READY
: 10 REM Spaces make line numbers line up, plus indenting
: 50 FOR A=1 TO 10
:100    PRINT "A=";A
:150    PRINT "A*A=";A*A
:150 NEXT A
:200 END
:LIST
 10 REM Spaces make line numbers line up, plus indenting
 50 FOR A=1 TO 10
100    PRINT "A=";A
150    PRINT "A*A=";A*A
150 NEXT A
200 END
:_

This can be abused:

                                                10 A=1
                              20 FOR B=1 TO 10
                     30 A=A*2
          40 PRINT A
50 NEXT B

One restriction is that lines that are named as a THEN/GOTO/GOSUB target must not have any leading spaces, otherwise the syntax check that runs at the beginning of a program run won't find the named line and will complain.

More fun can be had by abusing the Wang keyboard. The keyboard has an "index" and "reverse index" key. They correspond to HEX(0A) and HEX(0C), respectively. HEX(0A) is the linefeed character, and HEX(0C) is the reverse linefeed character. By typing

10 REM This <LF>code lives       <RLF>on two <LF>lines.

(<LF>=linefeed, <RLF>=reverse linefeed), the listing looks like:

10 REM This on two
code lives         lines.

For some reason, the first <LF> also causes a carriage return when it is listed. When you use the line editor to edit this line, things get mighty confusing.

OK, we're just getting started. <LF> and <RLF> would seem to be the only two magic characters that could be entered into a line. It turns out that the special function keys can be defined to insert a string literal, even during program entry. For example, enter this bit of code.

1000 DEFFN'0 "TAN(A)"

Whenever the special function key 0 is pressed, TAN(A) appears in a flash as if the user had typed it in keystroke by keystroke. This form of DEFFN' command is even more general, as HEX() constants can be used as well. This is where the abuse comes in.

1000 DEFFN'0 HEX(03)

HEX(03)is the clear screen command. We can use this feature to mess up the listing in arbitrary ways. Here's a simple one. "<SF0>" below means press the special function key 0 with the above mapping defined.

10 FOR A=1 TO 10:REM<SF0>
20 PRINT A;:REM<SF0>
30 NEXT A:REM<SF0>

This program can be run and it prints a list of the numbers 1 to 10. If you list it, or any line, the screen is cleared.

It is even possible to make the listing show an entirely different program by placing a decoy program after the <SF0>. For example, map <SF0> to be HEX(08), backspace, and enter this program (each "#" is the mapped backspace and each "." is a real space):

10 FOR I=1 TO 10:REM ####################for(i=0;i<10;i++) [
20 PRINT I;:REM #############..printf("%d ",i);
30 NEXT I:REM ###########]...........

When you run the program, it produces a list of the numbers 1 to 10, but when you list it, it looks like C code!

:LIST
10 for(i=0;i<10;i++) {
20   printf("%d ",i);
30 }
:_

#14 – Token on Something (link)

bug image

This builds on the previous trick, Spacing Out.

Wang BASIC internally uses codes HEX(00) to HEX(7F) to represent the ASCII characters of the program. They keywords of the language, such as PRINT , and END, even TO may be entered as sequences of ASCII characters, but when the interpreter first parses the line, these each get converted to a single byte representation. HEX(80) to HEX(FF) are used for encoding these keywords. This trick, common in many BASIC interpreters, saves a lot of space and makes subsequent interpretation much faster.

Some Wang keyboards have keywords as the shifted equivalent of some key on the keyboard, (say, SHIFT-P is PRINT ). All that is happening is that the one byte token representing a keyword is directly deposited in the command line, vs having to be converted later.

The bug comes about because not all codes are used. HEX(F0) to HEX(FE) apparently are not committed. Since the DEFFN'0 HEX(..) mechanism allows depositing an arbitrary byte in the input buffer, the bug becomes manifest.

1000 DEFFN'0 HEX(F1)

Then type

10 <SF0>
LIST

All sorts of garbage spews out, and you get a syntax error number. The syntax number can be avoided by putting the magic byte into a REM comment, or inside the quotes of a PRINT statement. Different codes produce different spew.

#15 – Hidden Code (link)

bug image

This trick builds (to a crescendo) on the previous two tricks.

Wang BASIC converts all line number references, such as the one at the beginning of each line, or after a GOTO, GOSUB, or THEN, to a three byte sequence, FF ab cd, where ab cd is the BCD representation of the line number. For example, line 1520 would be encoded as FF 15 20.

Using the same DEFFN'0 HEX(....) trick, we can insert arbitrary line numbers, including illegal ones.

10 PRINT "Normal code"
1000 DEFFN'0 HEX(FFC1C1)

This shows up as "<1<1" (a choice other than C1C1 would produce something else weird). Enter this illegal line by hitting <SF0> then entering your executable code:

<SF0> PRINT "Magic code here":RETURN
20 GOSUB <SF0>:END

When you LIST the program, the illegal line number doesn't appear, and that the reference to that line would appear to be a syntax error. Then when you RUN, the hidden line shows that is is still present.

:LIST
10 PRINT "Normal code"
20 GOSUB <1<1:END
1000 DEFFN'0 HEX(FFC1C1)
:RUN
Normal code
Magic code here
 
:_

Other illegal lines may or may not show up in the listing, depending on where it falls. If the chosen line is FF 00 CC, for example, it will appear between line 99 and line 100 of your listing. The reason the code didn't show up at all in the original example is that when you type LIST, the interpreter must silently convert it to be LIST 0,9999, and since C1C1 is after 9999, it doesn't show up. One can still get those hidden lines to list out by explicitly naming the line or giving a range using an even greater illegal line number.

#16 – Transposition Error (link)

Page 13 of the Matrix manual has a very specific warning about not doing a matrix transpose operation under some conditions:

Note:
With any 32K System 2200, an array to be transposed must not be the first variable or array defined in the program; it must be preceded in the program by a variable or array defined with at least 8* (column-dimension-of-array-to-be-transposed - 1) bytes.

That sounded like an invitation to me, so here is a sample program and the results:

:CLEAR
READY
:10 DIM A(3,3)
:20 DIM B(3,3)
:30 FOR R=1 TO 3:FOR C=1 TO 3
:40 A(R,C)=3*(R-1)+C
:50 NEXT C:NEXT R
:60 MAT PRINT A:STOP
:70 MAT B=TRN(A)
:80 MAT PRINT A:STOP
:90 MAT PRINT B
:RUN
 1               2               3
 4               5               6
 7               8               9
 
STOP
:CONTINUE
 0               0               0               0
 0               0               0               0
 0               0
 0               0               0               0
 0               0               0               0
 0               0
 ... (a 10x10 array of 0's is printed)
 0               0               0               0
 0               0               0               0
 0               0
 
STOP
:CONTINUE
 1               4               7
 2               5               8
-0.00010000E-;>  3               6
 
:_

The A(,) array has been redimensioned to a 10x10 array. B(3,1) is an illegal value, and it isn't really the transpose. Different sized arrays result in different illegal values. The illegal value doesn't appear to depend on the array values.

Update 2006/03/07:

Although the manual cautions only about TRN operation on a 32 KB system, other matrix operations are at risk too.

:CLEAR
READY
:10 MAT A=IDN
:20 MAT PRINT A
:RUN
 
:_   (despite the prompt, the machine is hung)

A warm reset won't fix the hang; it needs to be power cycled.

:CLEAR
READY
:10 DIM A(4,4)
:20 MAT A=CON
:30 MAT PRINT A
:RUN
 1       1       1       1
 1       1       1       1
 1       1       1       1
 1       1       1       1
 
:_

No problems there.

:CLEAR
READY
:10 DIM A(4,4)
:20 MAT A=IDN
:30 MAT PRINT A
:RUN

 1       0       0       0
 0       1       0       0
 0       0       1       0
 0       0       0       1
:LIST
(nothing there)
:_

Note that

10 MAT A=IDN(10,10)
20 MAT PRINT A

hangs, while

10 MAT A=IDN(9,9)
20 MAT PRINT A

is OK, and

10 DIM A(9,9)
20 MAT A=IDN(9,9)
30 MAT PRINT A

hangs. The unused portion of the array is what matters.

Explanation of Bug

What is magic about 32 KB, and why does it matter that the array be the first one declared? The 2200 CPU has a 16 bit address, but the unit of memory addressing is the nibble. Also, the variable table (where each entry in the table consists of the variable name and its value) is located at the end of memory. The variable appearing in the program will be located in the highest location in memory. Apparently some of the MAT routines do pointer arithmetic and don't handle on the possibility of pointers wrapping off the end of memory.

#17 – PRINT (ab)USING (link)

Here are some more oddities about the PRINTUSING and image statement that I've figured out.

10 PRINT "HI":% This is pretty much like a REM

In the example above, the image declaration (the "%" and later) can't be used by a PRINTUSING statement, as it must be the first statement after the line number, but in this atypical usage, it functions like a REM statement. When line 10 is executed, the PRINT happens, but the image is ignored.

The BASIC manual that image statements must not use field declarations for numbers that are wider than 16 digits. Let's see what happens.

:10 % ###.####################
:20 PRINTUSING 10, 123 + 1/3
:RUN
 123.33333333330001233333
 
:_

Explanation of Bug

You can see that the mantissa is internally represented as 16 nibbles. You can see that there are only 13 significant digits, with the trailing digits filled with zeros. Filling in leading "#"s with spaces or zeros as appropriate, each "#" of the PRINTUSING causes the next nibble to be printed. However, when there are more than 16 digits after the first significant digit, the nibble pointer wraps around and starts printing the digits again.

One other oddity of PRINTUSING is that if the width of the image exactly matched the declared width of the output device, then each PRINTUSING referring to the image would produce two line feeds after each statement.

#18 – Executing Buggy Code (link)

Whenever a program is going to be run, the BASIC interpreter does a pass through the program source to ensure that all line number references (GOTO, GOSUB, IF ... THEN, KEYIN, ON...GOTO, ON...GOSUB, PRINTUSING) are valid and that all PRINTUSING line references start with an image specification. If any line is referenced by the program but doesn't exist, the program doesn't even begin running. Also, if any line contains a syntax error, the program will refuse to run. I believe that first pass is also used to build up symbol tables for the variables used by the program. For larger programs, there is a noticeable delay from the time "RUN" is entered until the program actually does anything visible because of this first pass.

Normally, this checking is a good thing. With other BASICs, it is maddening to be running a program for a while only to hit a syntax error just because the code path never meandered onto that particular line in error. Sometimes, though, it is nice to be able to run a program even though there is an error. There is a way around the strict error checking.

Wang BASIC has an unusual feature called the "named GOSUB". Named subroutines can optionally take an argument list. A subroutine is marked with a integer label from 0 to 255, like this:

100 DEFFN'3(N)
110 PRINT "The square root of"; N; "is"; SQR(N)
120 RETURN

This can be called from within a program, like this

10 GOSUB'3(5)

What is particularly Wang-ish is that there are 16 special function keys (32 if used in combination with the SHIFT key), and these can be used to directly call the routine with whatever parameters are on the command line. This feature makes the 2200 act like a very sophisticated calculator.

:2 (then press special function key 3)
The square root of 2 is 1.4142135624
 
:_

To get back to the story, in order to make these keys as responsive as possible, Wang BASIC doesn't go through the usual syntax check. Bringing this all together, if you want to execute a program that has some syntax errors that you feel you can ignore for the time being, just start your program with a function key:

10 DEFFN'0:RETURN CLEAR
20 ... (the rest of your program on following lines) ...

Press special function key 0 to run the program. The first line "catches" the special function key 0 action, then tells BASIC to ignore the fact that it was in a subroutine and to just continue on.

Be warned, though, that the first pass that has been skipped won't be able to build up a proper symbol table, so if your program references any new variables, who knows what will happen:

:CLEAR
READY
:10 DEFFN'0:RETURN CLEAR
:20 PRINT "This is a test"
:30 A=1
:40 PRINT "A=";A
:50 PRINT "B=";B
:_(press special function key 0)
This is a test
A= 0
B= 1
 
:_

oops!

#19 – COMmotion and disArray (link)

bug image

This trick comes courtesy of Georg Schäfer, of Bergisch Gladbach, Germany. He writes:

Yesterday I played a little with your emulator (works great!) and especially wanted to try an old trick that I learned on our old Model B at School back in 1976. I wasn't sure whether this will work on a Model T, because my machine still waits for repair, and it doesn't work with BASIC 2 on my VP, but it did on your emulator!

It was the very, very first command I ever saw executed on the Wang and even on any computer, and I still can remember the moment, when a guy from another class showed it to us.

It was a 8 K machine, and he typed:

    CLEAR
    COM A$(87,87)1

You can try it on your emulator, it shows the same nice effect :-))).

For the 32 K machine you can use the following:

    COM A$(228,141)1

We called this special system error the "NESY-Error", because the name of the "inventor" was Jasper Neumann.

This causes the BASIC interpreter to repeatedly emit

    SYSTEM ERROR!
    :
    ^ERR 01

until the machine is reset. Using DIM instead of COM does not have the same effect.

On the 32 KB machine, END reports 32070 bytes free, yet 228*141*1 is slightly more than this at 32148 bytes. Using COM A$(114,141)2 or COM A$(57,141)4 also trigger the bug.

#20 – Array Dumbensions (link)

bug image

This is a bug that is related to the previous trick – by dimensioning an array slightly bigger than what is allowed, the interpreter corrupts itself. On a 32KB 2200T CPU, type the following:

:CLEAR
READY
:10 DIM A$(200,10)16
:20 DIM B$(4)20
:30 PRINT "This is line 30"
:40 PRINT "This is line 40"
:RUN
This is line 30
This is line 40

:
^ERR 01
:LIST
10 DIM A$(200,10)16
20 DIM B$(4)20
30 PRINT "This is line 30"
:_

What happened to line 40? Run it again and line 30 disappears. If the DIM statements are COM statements instead, the results are a bit different.

#21 – The Point of No Return (link)

bug image

While disassembling the Wang 3300 Extended BASIC interpreter, I spotted a bug. I also knew that even though the 3300 CPU and the 2200 CPU were very different, the BASIC interpreter implementations have a lot in common. So I tried out the following code, and indeed the 2200 has the same bug:

:CLEAR
READY
:10 GOTO 30
:20 RETURN
:30 GOSUB 20
:RUN

At this point text is spewed out and an ERR 15 message appears (although the error message might change depending on the exact state of the memory when the program runs).

I strongly suspect Stupid Trick #1 is a manifestation of the same bug.

:CLEAR
READY
:10 FOR I=1 TO 10
:RUN
:NEXT I

Explanation of Bug

When the interpreter sees the GOSUB statement, it locates the start of the next statement, pushes that location on a stack, then jumps to the subroutine; when the RETURN is seen, this stack is popped of any unfinished FOR loops, then the return address of the GOSUB is retrieved and execution resumes there. In the code above, there is no next line to return to – some meaningless address is pushed on the stack and RETURN causes the interpreter to resume interpretation there.

In the FOR code example, when the interpreter sees the FOR loop, it pushes a small data structure on a stack; this structure contains a token identifying the structure as belonging to a FOR loop, the address of the index variable (so the symbol table doesn't have be searched on each iteration), the end and step values, plus the address of the instruction immediately after the FOR instruction. If there is no ":" after the FOR command, the interpreter grabs the address of the next line, but there is none, just like in the GOSUB bug described here.

#22 – Transdimensional Array Assignment (link)

In Wang BASIC, matrices are declared to have either one or two dimensions. I've always thought of the MAT operations as working on 2D arrays, but it is also legal to use some of them on 1D arrays (also known as vectors) if it makes sense:

:10 DIM A(5)
:20 MAT A=CON
:30 MAT PRINT A
:RUN
 1
 1
 1
 1
 1
 
:_

Some operations, like TRN, IDN, and INV require 2D arrays in general, but surprisingly the following is legal:

10 DIM A(1),B(1)
20 MAT B=IDN
30 MAT A=TRN(B)

This roundabout way to compute a reciprocal:

:CLEAR
READY
:10 DIM A(1)
:20 DIM B(1)
:30 A(1)=3
:40 MAT B=INV(A)
:50 PRINT B(1)
:RUN
 .3333333333333
 
:_

Lastly, there is this oddity, where a 1D array is assigned to a 2D array.

10 DIM A(1)
20 DIM B(1,1)
30 MAT B=A

Explanation of Bug

The symbol table entry for an array consists of a field indicating that the item is a 1D or 2D numeric array, a byte holding the first letter of the variable name, a nibble holding the optional second character of the name, total array size, first array dimension, second array dimension, then all the bytes required for the array values. Note that even vectors (1D arrays) have a slot holding the 2nd dimension, which is set to the value 1. In some instances the interpreter checks to see if the variable is 1D or 2D by looking at its symbol table type. In other cases it simply assumes it is a 2D array that happens to have a "flat" second dimension. For things like CON and ZER, that works out fine with no surprises, but that shortcut does lead to this weirdity.

#23 – Nothing For All Intents (link)

There is an odd thing in BASIC-2 Language Reference Manual, pointed out by Steve Powell, KCML expert. If you go to the index, there is a single entry under the letter Y, on page 451. The entry is "yurt", which is a domed tent used by the nomadic people of central Asia. The reference is to page the next page, 452, but it is blank. What could it mean?

I note that all other letters of the alphabet have at least one entry. Was this perhaps a placeholder? Or a workaround for some bug in their indexing software?

#24 – Your Code Here (link)

If you recall any bugs in Wang BASIC, surprising features, or abusive coding practices, I'd like to hear about it.