This is an implementation of a BASIC interpreter providing a BASICODE runtime environment independent of any emulator (unless you count the Java VM as one). The point of this project is to have such a thing, rather than providing a highly optimised BASICODE run-time environment. Frankly, the JVM, off the shelf, will do a good enough job at delivering run-time performance. This is also why the BASICODE implementation does not sport any translation to Java bytecodes. The whole thing should be simple, first and foremost.
If you're not interested in building the project before trying it, you can
download an executable JAR file (basicode.jar) from the
release page.
The project was built using Java 21 and Maven. Once those are installed, just
running mvn package on the command line should be enough to build
everything and run the tests.
The mvn package run will generate an executable JAR file named
basicode.jar in the project root. This file includes all relevant
dependencies, so it can be used as a standalone "executable" for BASICODE as
long as you still have Java 21 installed.
To run a BASIC file from the command line at the project root, run this (the
-hold argument will make the BASICODE window stay after the program has
run, until you press a key):
$ java -jar basicode.jar -hold examples/hello.bas
This works for other BASICODE programs, as long as you point to the right
basicode.jar and BASIC source file. If no BASIC source file is given, a
dialogue will open that lets you choose one.
In case the interpreter should display a splash screen before running the
actual program, use the -intro argument. The splash screen will show until
a key is pressed.
Some programs of yore kind of relay on the hardware being inherently slower than today's machines. This is particularly true for games with a high amount of interaction. These will typically run so fast on this BASICODE interpreter that they become unplayable.
If you want programs like these to be usable, or if you just want to
experience the feeling of 8-bit hardware execution speeds again, you can use
the -slow command line argument. If this argument is given, the
interpreter will pause execution after each statement for a multiple of 5 ms.
The actual slowdown depends on the number of o characters given in the
-slow argument. For example, just passing -slow will pause execution for
1 ms after each statement; passing -slooow will pause execution for 3 ms after
each statement.
In addition to -hold, -intro, and -slow (see above), you can use the
following command line arguments:
-nowait: suppress waiting. If this argument is given, theGOSUB 450subroutine will have no effect; execution will simply continue without delays.-nosound: play no sounds at all.-enforceBoundaries: do run-time checks that ensure theHOandVEcoordinates in graphics mode are always within the allowed0..1range.-showMapKeys: display debugging information when keys are pressed and mapped.
The BASICODE implementation is meant to be simple, and to be as "standalone" as possible. Therefore, the implementation relies on out-of-the box components of the JDK as much as possible. For the GUI, the standard AWT and Swing packages are used.
The BASIC grammar is located in the file basic.jj in the
de.haupz.basicode.grammar package. This file is there only for illustration
and for historical reasons: the parser used to be generated. It is now a
handwritten recursive-descent parser in the de.haupz.basicode.parser package.
While there are numerous BASIC dialects that do not use line numbers (or make them optional), the BASICODE implementation here is meant to be compatible with the BASICODE sources "out there". Therefore, the grammar here enforces the use of line numbers.
Several other restrictions the BASICODE conventions impose - e.g., the maximum length of a source code line being 60 characters - are not enforced.
The grammar, during parsing, generates an abstract syntax tree (AST) of the program. The nodes of this AST are directly used by the interpreter (see below).
The AST nodes generated by the parser contain all the logic that is needed to run the program they represent. The BASIC interpreter used here thus embodies an AST interpreter design.
The main interpreter loop is contained in the ProgramNode class, which can
be found in the de.haupz.basicode.ast package. Many of the AST node
come with a piece of JavaDoc commentary that describes how they work, so
I'll refer to those for further details.
BASICODE supports reading from and writing to files on different kinds of media that were in use at the time when it was invented. These include cassette tapes, floppy disks, and microdrives. Since this BASICODE implementation would typically not have access to such media, it defaults to always opening files on the file system the implementation is running on.
If file names are given without any directory prefix (which would be unusual
in BASICODE to begin with), files will be expected to be located, and can be
expected to be created, in the directory the java command to start
BASICODE was issued.
Similarly to file I/O, printer output is also emulated. The BASICODE
implementation will open a file named BASICODE-printer.txt that will
contain any output sent to the printer.
The grammar and basic functionality of the BASICODE implementation are
covered in a decent amount of unit tests, which can be found in the test
directory. The mvn test command will run them after building the project,
and building the standalone JAR using mvn package will run the tests as
well.
Some BASICODE example programs can be found in the examples directory.
They are mostly simple, and serve to test particular aspects of the BASICODE
conventions.
A vast set of examples can be found on Rob Hagemans' BASICODE in the browser page, or in his GitHub repository dedicated to BASICODE examples.
This BASICODE implementation takes advantage of the fact that it's written in Java and there are multiple unused line numbers below 1000 that can be used for additional subroutines.
Call these subroutines to dump all (960) or a selection of (961)
variable values and array contents to the Java console. For GOSUB 961, the
OD$() array controls what is dumped. Each element of that array should be
the name of a variable or array the values or contents of which should be
displayed. The OD$() array must be declared and properly dimensioned before
the first use of this subroutine. If it is not present or unusable, all
variables and arrays will be dumped.
Whenever this subroutine is called during execution, the current BASICODE call stack will be printed to the Java console. Execution will not be paused as would be the case with a breakpoint (see below).
Use this to trigger a breakpoint during execution. When this subroutine is
called, a dialogue box will open that displays the current call stack and
the values of all variables and contents of all arrays. If, at the time the
subroutine is called, the string OC$ contains a BASICODE condition that
evaluates to false, the breakpoint will not be triggered. If the string is
undefined or empty at that time, that counts as the condition being true.
Similarly to GOSUB 963, this will trigger a breakpoint during execution.
The variable values and array contents displayed will however be governed by
what is contained in the OD$() array. Each element of that array should be
the name of a variable or array the values or contents of which should be
displayed. The OD$() array must be declared and properly dimensioned before
the first use of this subroutine. If it is not present or unusable, all
variables and arrays will be displayed.
If, at the time the subroutine is called, the string OC$ contains a
BASICODE condition that evaluates to false, the breakpoint will not be
triggered. If the string is undefined or empty at that time, that counts as
the condition being true.
Calling this subroutine registers a watchpoint. It will be triggered when a
condition flips from "unmet" to "met". The condition is expressed in BASICODE
syntax in the OC$ variable. After execution of the subroutine, the
variable OP will contain a running number of the watchpoint. Numbering
starts at 1. A value of -1 indicates an error during registration; in that
case, OE$ will also contain an error message.
The watchpoint will be triggered whenever the condition flips from "unmet" to
"met" after the execution of a statement. It will honour the contents of the
OD$() array for selective display of variable values and array contents,
as described above.
Call this subroutine to register a breakpoint that will trigger without an
explicit call to GOSUB 963. Before calling this subroutine, set OL to
the line where the breakpoint should be triggered, and OS to the number of
the statement (starting at 0) before the execution of which the breakpoint
should be triggered.
Optionally, pass OD$ to control which variable values and array contents
should be displayed when the breakpoint is triggered; and use OC$ to
define a condition. See above for details on both of these.
After the invocation of the subroutine, OP contains the running number of
the registered breakpoint (starting at 1), or -1 in case anything has gone
wrong. In the latter case, OE$ will also contain an error message.
Any breakpoint registered with GOSUB 966 can be selectively (de)activated by
calling one of these two subroutines. They both expect the breakpoint in
question to be identified by the OP variable. A call to GOSUB 967 will
activate the breakpoint, and a call to GOSUB 968 will deactivate it. In
case of success, OP will be unchanged after the subroutine call. In case
of any kind of error, OP will be set to -1 and OE$ will contain an error
message.
If you're interested in contributing to this project, please find details in
CONTRIBUTING.md.
This project is under the MIT Licence, non-contagious, well-intentioned, and harmless.
The font used in the emulated console is "Amstrad CPC464 Regular" by Wesley Clarke. It's included here thanks to being licenced with CC BY-SA 3.0.
I'm immensely grateful to Thomas Rademacher of basicode.de for introducing me to BASICODE, and for contributing the splash screen. Many thanks to Bernd Bock and other members of Joyce-User-AG for encouragement and bug reports.