https://t3x.org/nmhbasic/index.html
http://t3x.org/nmhbasic/ (light|dark)
---------------------------------------------------------------------
NMH BASIC Logo
NMH BASIC
Download: nmhbas23c.zip (version 1.2, 74KB) | nmhbas25c.zip
(version 2.1, 90KB) | 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
4. NMH BASIC II
5. NMH BASIC III
Programs
Minesweeper screenshot
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.
Floodfill demo screenshot
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 4700bytes 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 12Kbytes 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.
There is also a simplified version of the interpreter that runs under
CP/M. A COM file for CP/M (BASICS.COM) is also included in the
archive. The CP/M version currently needs 32K bytes of TPA to run.
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 12Kbytes 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 here: 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.
Listings
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.
NMH BASIC II
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.
NMH BASIC III
Download: nmhbas3_30.zip (version 3.0, 105KB) | man page
Version 3.x of NMH BASIC is an in-progress version that differs from
the previous versions in a few points that are described in detail in
the manual. Most prominently, the CMPS ("compare strings") function
has been replaced with string comparison operators, so, for example,
IF CMPS(A$, 'FOO') = 0 PRINT 'YEP'
would now be written as
IF A$ = 'FOO' PRINT 'YEP'
Then, NMH BASIC III supports baudot-encoded units. This means that
any unit connected to the interpreter can be written to and read from
using five-channel baudot-encoding (CCITT-2, US-TTY). So the
interpreter can, in theory, save and load programs to/from five-hole
paper tape.
The T3X/0 version of the interpreter is currently fully working.
There also is a more efficient Z80 version written in assembly
language, which is work in slow progress. It currently runs all the
example programs, but lacks baudot-encoded units and may still have a
few bugs. An 8086 version written in assembly language may appear
later.
---------------------------------------------------------------------
contact | privacy