http://t3x.org/nmhbasic/

NMH BASIC

NMH BASIC

Download: nmhbas23c.zip (74KB)  |  man page

This is a small BASIC interpreter that I wrote in the early 1990s. For some reason I think it is one of the coolest programs I have ever written. Maybe because it is just a bit under 5K bytes large and still does something useful. Maybe it is just nostalgia.

  1. Programs
  2. Implementation
  3. Hacks and Quirks : Arrays | Input/Output | Conditional Statements | Listings

Programs

One of the more interesting programs I have written in NMH BASIC is a variant of the well-known Mine Sweeper game that runs in text mode. Not just text mode, actually, but (tele)typewriter mode, as it reprints the playing field after every move.

The screenshots use Viacheslav Slavinsky's excellent GlassTTY font, a TrueType font that perfectly resembles the one used in the DEC VT-220 terminal. The same font, at bigger magnification, is used in the NMH BASIC logo.

What is maybe interesting about the mine sweeper clone is that it uses a stackless floodfill algorithm that stores its state in the playing field itself and needs no dynamic memory at all. I have recently described it in the paper A Stackless Floodfill Automaton (PDF, 34KB). A demo showing an animation of the algorithm is included in the NMH BASIC package.

There are other programs in the package, most of them rather simple, like an implementation of the Hangman game, the (rather pointless) Nim game, a banner printer, a random number generator, etc. NMH BASIC does not have a RNG, so a 15-bit linear feedback shift register is implemented in BASIC to generate pseudo-random numbers.

The first program I have ever written in NMH BASIC was the inevitable prime number sieve. I have no idea how often I have loaded and run it in the past decades — until the Floodfill demo became my new favorite. Here is the code of my first NMH BASIC program (the backslash is the division remainder operator):

 10 REM 'PRINT PRIME NUMBERS'
 20 REM 'M = NUMBER OF PRIMES TO PRINT'
 100 LET M = 1000
 105 DIM Z ( M )
 110 LET Z ( 0 ) = 2 : LET T = 1 : LET P = 1
 115 PRINT 2 ,
 120 IF T >= M GOTO 200
 130 LET P = P + 2 : LET O = 1
 140 FOR I = 0 TO T - 1
 150 IF P \ Z ( I ) = 0 LET O = 0 : LET I = T
 160 NEXT
 170 IF O = 0 GOTO 120
 180 LET Z ( T ) = P : LET T = T + 1
 185 PRINT P ,
 190 GOTO 120
 200 END

Implementation

I wrote the first version of NMH BASIC in 1994, recycling some parts that I had written in the years 1991..1993. The first version that I wrote in 1994 was a prototype in BASYL-II which I then translated, function by function, to 8086 assembly language. The resulting executable had a size of about 4700 bytes and because the token representation that the interpreter uses internally is quite efficient, you could do interesting things with NMH BASIC in as little as 12K bytes of memory. I had named the interpreter 12K-BASIC initially, but soon learned that others had had that idea before me.

Of course in 1994 memory was already measured in megabytes, so you might say that writing a tiny BASIC interpreter was kind of pointless at that time. It depends I would say; it is better than getting drunk in a bar, and now, almost 30 years later, I still enjoy playing with this little program. So much, in fact, that I decided to translate the original code to T3X/0, so that I can play with it on Unix without having to use an emulator.

All the above versions are included in the package: the original BASYL-II version, the assembly language version, and the new T3X version. You can recompile the T3X version with T3X/0 and the 8086 assembly language version with TASM or MASM. You need to create a COM file or it will not run. A precompiled COM file and Tcode machine executable (as well as a Tcode machine for Unix) are also included in the package.

Hacks and Quirks

Arrays

The NMH BASIC language contains some interesting (IMHO) hacks to make its implementation simpler.

All variables have either single-character names or names consisting of a character and a digit, like A0, B2, Z9, etc. The expressions A and A0 and A(0) all refer to the same variable. If you do not use A0..A9, you can use A as a 10-slot array A(0) .. A(9). Or you can use A5 in the place of A(5) if you are refering to a fixed slot.

It is getting even weirder. A(10) is the same as B or B0 or B(0). A(22) is equal to C2 or C(2) and, finally, A(259) would be equal to Z(9). So, for instance, if you do not use any Z's, you can use Y as a 20-slot array. In this case the command DIM Y(20) is really a null-operation. It merely serves as a reminder that Y is a 20-slot array (and Z should not be used).

You could also use Y as a 50-alot array by dimensioning it with DIM Y(50). In this case the elements of Z will still be used, but 40 additional slots will be allocated to integer variable storage, so Z becomes a 40-slot array and Y a 50-slot array. It probably goes without saying that most programs either use single-character variables as 10-slot arrays or dimension Z, if a larger array is needed.

This also means that DIM Y(50) and DIM Z(50) in the same program would just allocate 50 integer slots to Z and the last 40 slots of Y would overlap with the slots of Z. Having multiple large arrays in the same NMH BASIC program requires some hacking, like using Z(0)..Z(99) for one array and Z(100)..Z(199) for the other.

Note the definition of "large" above. NMH BASIC uses 12K bytes of memory in total: for integer variables, string variables, program memory, stacks, and the machine code of the interpreter itself! You could probably write a version of this interpreter that would run on a CP/M machine with as little as 16K bytes of transient program area (but I have never done so).

Input/Output

The interpreter performs I/O on "units", where each unit is assigned a file or device when the interpreter is started. NMH BASIC programs cannot open or close any files. They can only redirect input and output to the assigned units. I/O is sequential, i.e. each unit is like a tape drive. The following program prints the data stored on unit #5:

100 LET X = IOCTL( 5 , 100 ) : INPUT #5
110 INPUT A$ : IF ASC( A$ ) = 255 INPUT #0 : END
120 PRINT A$ : GOTO 110

The statement INPUT #5 redirects input to unit #5, so from that point on all INPUT statements will read from that unit. (Analogously, PRINT #5 would redirect output to unit #5.) When a string read from a unit contains the value 255 in its first slot, there is no more input available from the current input unit. INPUT #0 connects input back to the keyboard. Note that PRINT #1 would connect output back to the screen.

There is an IOCTL function that can perform several "services" on a unit, like rewinding it, appending to it (moving the read/write pointer to the end of the unit), or truncating it (or writing an EOF marker on a tape). The IOCTL call in the above example rewinds the unit.

The maximum length of a line or string is 64 bytes. Reading anything longer, either via INPUT or by entering it at the interpreter prompt, will result in an error. The CR,LF characters that separate lines are not counted.

Conditional Statements

I have forgotten how other BASIC dialects handle this, but I suspect that NMH BASIC is the odd one out. In an IF statement the entire rest of the line is executed conditionally. For example

IF 1 = 1 PRINT 'FOO' : PRINT 'BAR'

would print both FOO and BAR. There is no THEN or ELSE keyword. The first keyword after the condition of IF starts the conditional part of the IF statement. When the condition in IF is false, the interpreter advances to the next line. An alternative branch is implemented with jump around jumps using GOTO:

100 IF condition GOTO 130
110 alternative statements
120 GOTO 140
130 consequent statements
140 REM

Or, if the condition and statements are short:

100 IF condition statements
110 IF # ( condition ) statements

The # operator implements the logical NOT. It has high precedence, though, so the condition often has to be parenthesized. There is a logical AND, but not a logical OR in IF. If there are multiple conditions separated by commas then the conditional statements will only execute, if all conditions are true. For example, the statement

IF 0 < C , C < 11 STOP

will stop program execution, if C is in the range 1..10. To implement a logical OR, multiple IF statemements with the same conditional part have to be used.

Listings

NMH BASIC lists programs with blanks between adjacent tokens. This is merely a characteristic of the LIST routine, though. You can enter a program as

FOR I=1TO10:PRINT A(I):NEXT

but the LIST command will print it as

FOR I = 1 TO 10 : PRINT A ( I ) : NEXT

This has the weird side effect that sometimes you can SAVE a program but cannot LOAD it later, because some lines will be shorter than 64 characters when you enter them, but LIST (and hence SAVE) will blow them up to a bigger size. For example:

100 IF ASC(MID$(A$, I, 1)) = ASC('X') LET X = X+1 : GOTO 120
----+----1----+----2----+----3----+----4----+----5----+----6---|
100 IF ASC( MID$( A$ , I , 1 ) ) = ASC( 'X' ) LET X = X + 1 : GOTO 120

There is no workaround. When a program cannot be loaded, the only remedy is to edit it with a text editor and fix it, either by removing unnecessary blanks or, even better, by splitting the offending line. E.g.:

100 LET C = ASC( MID$( A$ , I , 1 ) )
105 IF C = ASC( 'X' ) LET X = X + 1 : GOTO 120

Finally, it is a good idea to keep NMH BASIC programs in DOS text format with CR/LF line separators, even on Unix systems, because otherwise the DOS version of the interpreter will refuse to load them.


contact  |  privacy