Tinycat BASIC is a line-number BASIC dialect that can be implemented in less than a thousand lines of Java or Go (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.
Tinycat BASIC has a number of useful additions on top of its model:
- floating point numbers;
- arbitrary variable names;
- control structures like DO ... LOOP and FOR ... NEXT;
- the logical operators: AND, OR, NOT;
- power and flooring division operators;
- functions, both built-in and user-defined*;
- random number generation.
*) Note: the new Go implementation lacks DEF FN.
Some features would not be trivial to add, and therefore outside the scope of this project:
- string variables.
Others can be added easily, but would cause more trouble than it's worth:
- multiple statements per line; they require littering the source code with special cases, and lower performance for little benefit.
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 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
All three editions of the interpreter can be embedded, with some caveats:
- the Python edition will only have one shared context;
- only the Java edition has independent per-context random number generators;
- the Go edition has a (trivial)
main()function that must be replaced.
Both the Go and Java interpreters support I/O redirection, but only the latter does it per-context.
Extending Tinycat BASIC
The Java implementation is fully extensible: by subclassing the interpreter, you can add more statements, built-in functions and even expression kinds!
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.
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)*
Bugs and caveats
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.