Basic interpreters

Tinycat Basic

Tinycat BASIC is a line-number BASIC dialect that can be implemented in less than a thousand lines of Java, Go or D (barring extensions to the core). It achieves that by relying on direct interpretation and having no type system. Its name is meant to honor the classic dialect Tiny BASIC as inspiration and ancestry.

Rationale

While the hardware limitations that made Tiny BASIC desirable are largely a thing of the past, having programming language dialects that can be trivially implemented remains a good idea. It reduces software dependencies and black box components.

You can also think of it as an art project, and/or a comparative study in programming language implementation.

Download

Tinycat Basic 1.1 (54.7K) is free and open source software under the Artistic License 2.0; see the included text file for details.

A copy of the manual page is included in the package.

Features

Tinycat BASIC has a number of useful additions on top of its model:

*) Note: the Go implementation lacks DEF FN.

Some features would not be trivial to add, and therefore outside the scope of this project:

Others can be added easily, but would cause more trouble than it's worth:

Performance

Note: the figures below have changed repeatedly as the interpreters were improved and flaws were fixed in the benchmark itself. Consider them a rough indication of relative performance.

The reference Python implementation is 113 times slower than the host language. Conversely, the Go implementation is 47 times slower than native code, within limits for this type of interpreter. The D interpreter starts out 49 times slower than native code, but becomes only 43 times slower when optimized (it gains more from optimization than the native benchmark). In absolute terms however it's slower than Go by roughly 35%. Compilers other than dmd should yield better results.

The Java implementation proved harder to benchmark, as a long-running interpreter runs progressively faster. That said, it seems to be roughly 47 times slower than pure Java on a fresh start (7 times slower than the Go implementation in absolute numbers), but ends up only 2.6 times slower than a compiled Java program. That's almost as if the interpreter wasn't in the way anymore, and actually three times faster than the Go implementation!

Overall, Java seems the best suited for interpreting another language. Or at least this interpreter architecture happens to suit Java unusually well.

Embedding Tinycat BASIC

Most editions of the interpreter can be embedded, with some caveats:

The D and Java interpreters support per-context RNGs and I/O redirection.

Extending Tinycat BASIC

The D and Java implementations are fully extensible: by subclassing the interpreter, you can add more statements, built-in functions and even expression syntax!

The Python implementation can be extended with new statements or functions.

In the Go implementation, you can only add more built-in functions without changing the source code, even if you convert it to an importable package.

Supported commands

LIST
RUN
CONTINUE
CLEAR
NEW
DELETE line-number
LOAD "filename"
SAVE "filename"
BYE

Commands are only available at the built-in command prompt. It is assumed that a program embedding the interpreter will provide its own alternatives.

The BYE command leaves the command loop and returns to the host application (which simply closes a stand-alone interpreter). You can also press Ctrl-D to send an end-of-file character.

Supported statements

LET name "=" expression
IF expression THEN statement
GOTO expression
PRINT (string | expression)? ("," (string | expression))* ";"?
INPUT (string ",")? name ("," name)?
FOR name = expression TO expression (STEP expression)?
NEXT name
GOSUB expression
RETURN
DO
LOOP (WHILE | UNTIL) expression
REM text
DEF FN name "(" (name ("," name)?)? ")" "=" expression**
RANDOMIZE expression?
STOP
END

**) Note: absent in the Go edition.

Built-in functions

TIMER()
RND()
PI()
INT(n)
ABS(n)
SQR(n)
SIN(n)
COS(n)
RAD(n)
DEG(n)
MIN(a, b)
MAX(a, b)
MOD(a, b)
HYPOT2(a, b)
HYPOT3(a, b, c)
IIF(a, b, c)

Beware that the IIF function doesn't short-circuit (and neither do the logical operators).

Expression syntax

expression ::= disjunction
disjunction ::= conjunction ("or" conjunction)*
conjunction ::= negation ("and" negation)*
negation ::= "not"? comparison
comparison ::= math_expr (comp_oper math_expr)?
comp_oper ::= "<" | ">" | "<=" | ">=" | "<>" | "="

math_expr ::= term (("+"|"-") term)*
term ::= factor (("*" | "/" | "\") factor)*
factor ::= ("+"|"-")? (number | name | funcall | "(" expression ")")
funcall ::= name ("(" expr_list? ")")?
expr_list ::= expression ("," expression)*

Bugs and caveats

Java always displays numbers with at least 6 digits of precision. It seems to be a bug in String.format().

Python always displays numbers with at most six digits of precision -- the exact opposite behavior!

For portability, programs making use of randomness should call RANDOMIZE near the beginning. Not all implementations do that by default.

The Go implementation is abandoned as of October 2018.