Download: nmhbas23c.zip (version 1.2, 74KB) | nmhbas24a.zip (version 2.0, 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.
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
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.
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).
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.
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 had high precedence in NMH BASIC up to version 1.2, but has very low precedence in NMH BASIC II. Interestingly, this change did not affect any programs in the archive. 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 (or jumps around jumps) have to be used.
NMH BASIC 1.x listed programs with blanks between all adjacent tokens. NMH BASIC II uses a more compact representation. Either way 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
in NMH BASIC and as
FOR I = 1 TO 10 : PRINT A(I) : NEXT
in NMH BASIC II.
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.
This is mostly a problem in NMH BASIC 1.x, which inserts blanks between all tokens. 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.
In December 2024 I changed a few things and pubished a new version
of NMH BASIC, which I called, for lack of imagination, NMH BASIC II.
The new version changes the precedence of the #
(logical
NOT) operator from highest to lowest (this was a mistake in the original
version!) and uses a more compact and more comprehensible LIST
format, which is also used for saving programs. Because some code was
simplified in the interpreter at the same time, the new version is one
byte smaller than the original version.