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.
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.
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.
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:
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.
Most editions of the interpreter can be embedded, with some caveats:
The D and Java interpreters support per-context RNGs and I/O redirection.
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.
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.
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.
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 ::= 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)*
Java always displays numbers with at least 6 digits of precision. It seems to be a bug in
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.