ABASIC is a simple Rust-based BASIC interpreter.
This interpreter was made primarily for learning and personal edification. It is missing features from mainstream implementations of the language, so if you want a full-featured BASIC, you should probably look elsewhere.
For more details, see Rationale.
You can use the web version at https://toolness.github.io/abasic/.
Here are some programs you can run with it:
- Chemist
- Hamurabi - For more on this game's history, see its Wikipedia page.
Install the executable with:
cd abasic-core
cargo install --path=.
(Alternatively, you can run the install script in the root directory.)
You can run the BASIC interpreter interactively with:
abasic
Or you can run a program:
abasic programs/chemist.bas
Use abasic --help for more details.
The root directory is a workspace with a few different packages:
abasic-coreis the core BASIC interpreter. It doesn't do anything UI-related.abasic-cliis the command-line version of the interpreter.abasic-webis the Web version of the interpreter.
To run tests, from the root of the project, use:
cargo test
To run the command-line interpreter, use:
cargo run
Note: If you're on Windows, to use the dev server you will need to enable support for symlinks. See this StackOverflow post for more details.
To develop for the Web you will need to cd into abasic-web.
Whenever you make any changes to Rust code, you will need to rebuild the WebAssembly binary and its associated JS and TypeScript typings:
npm run wasm
Iterating on the rest of the Web front-end can be done by running:
npm run dev
Then navigate to http://localhost:8080/.
You can run individual programs by using the p querystring argument
set to the stem of a file in the programs subdirectory, e.g.:
http://localhost:8080/?p=chemist
To deploy the Web frontend, you can run:
npm run publish
An experimental Language Server Protocol server and VSCode client
is in the abasic-lsp directory.
To use develop it in VSCode:
-
In the root of the repository, run
cargo build -p abasic-lsp. -
In VSCode, open the root of the repository and press F5.
-
In the newly-opened Extension Development Host VSCode instance, open the command palette and choose "Output: Show output channels...", then choose "ABASIC Language Server". This should show the stderr of the language server.
-
Open or create a .BAS file. As you type into it, you should see warnings and errors appear (for example, try leaving out a closing double-quote on a string, or GOTO a non-existent line).
If you make changes to the Rust server code, you'll need to rebuild it. You will probably need to quit the Extension Development Host so Cargo can actually overwrite the server executable.
If you make changes to the VSCode extension, they should be automatically re-transpiled to JS, though you will need to restart the Extension Development Host to see them take effect.
To install the extension to your local VSCode so you can actually use it normally:
cd abasic-lsp
npm run ext
Note that you may need to quit VSCode to do this, if it's already running the LSP server.
To use the LSP server with Helix:
-
In the root of the repository, run
cargo install --path=abasic-lsp. -
Add the following to your
languages.toml(create it if it doesn't exist) in your Helix configuration directory:[[language]] name = "abasic" scope = "source.abasic" file-types = ["bas"] language-servers = ["abasic-lsp"] roots = [] [language-server.abasic-lsp] command = "abasic-lsp"
For more details, see the Helix Languages documentation.
Now when you open any BASIC file in Helix, it will use the LSP.
To see the stderr logs of the LSP server in Helix, you can run hx -v
and then :log-open.
I created this interpreter with the following goals:
-
I programmed BASIC as a child but have largely forgotten it. I also didn't fully understand the syntax and semantics back then. I've encountered the language recently when attempting to learn 6502 assembly for the Apple II and have been confused by some of its more idiosyncratic aspects, so I thought writing my own interpreter would help me learn the language better.
-
I've been reading about historic software in books like 50 Years of Text Games, The Apple II Age, and 101 BASIC Computer Games and thought it'd be fun to make an interpreter capable of running some of those programs.
-
I hadn't written anything in Rust in a while and wanted an excuse.
Note that this list isn't exhaustive.
DEF(user-definable functions)LETIF ... THEN ... {ELSE}FOR ... TO ... {STEP} ... NEXTGOTOGOSUBREMPRINT/?INPUTREAD,RESTORE, andDATADIM(arrays)- Arithmetic expressions (
+,-,*,/, and^) - Logical operators (
AND,OR,NOT) - Floating point and string values
- Line crunching (e.g.,
10PRINT123is semantically identical to10 PRINT 123) :(used to execute multiple statements in one line)
The interpreter also supports a number of debugging features inspired by Applesoft BASIC:
-
STOPis similar to JavaScript'sdebuggerinstruction, and will pause a program's execution. At this point, the program state can be inspected and changed.CONTcan be used to resume execution. -
Pressing CTRL-C during execution will pause a program's execution at whatever line it's currently on. At this point the program can be inspected and resumed with
CONT. -
TRACEcan be used to show the line number of each statement as it's being executed. To disable the feature, useNOTRACE.
There's a lot of things that haven't been implemented, some of which include:
WHILE ... WEND,REPEAT ... UNTIL,DO ... LOOPON ... GOTO/GOSUB- Scientific notation (e.g.
1.3E-4) - Integer types via the
%suffix (e.g.C% = 1) MAT(matrices)
-
BASIC was never standardized (attempts were made, but they never succeeded). Because of this, users inputting BASIC programs listed in books and magazines had to have some knowledge of BASIC: publications usually included tips on modifications needed to make their programs work on individual platforms, but always told users that they needed to read their platform's BASIC manual and deal with idiosyncrasies as they encountered them.
This gave me a surprising amount of creative freedom in implementing ABASIC. While I usually hewed to the behavior of Applesoft BASIC, I also examined the behavior of Dartmouth BASIC and sometimes chose behavior based on whatever seemed most user-friendly or historically accurate.
-
Reading about the history of BASIC made me realize that line numbers were actually a vital affordance, as the language was invented during a time when computers didn't have screens--they just had keyboards and printers. Essentially, communicating with a computer back then was a bit like talking via SMS text messaging today.
This meant that document editors couldn't really exist. In lieu of that, assigning numbers to lines and allowing them to be redefined interactively actually allowed for a surprisingly fast feedback loop.
-
ABASIC provides no limitations on variable and function names. This is unlike most first-generation BASIC interpreters, which only permitted short variable names, likely to preserve memory (Applesoft actually permitted long names but ignored everything after the first two characters).
At first I assumed this was an unquestionably good thing, but then I realized that since original BASIC ignores whitespace, long variable names can lead to surprising syntax errors, because the likelihood of a reserved word like
IForNOTappearing somewhere in a variable name increases as its length grows. This is probably part of why later BASICs abandoned line crunching. -
ABASIC's interpreter uses a recursive descent parser, evaluating the code as it's being parsed. In other words, there is no abstract syntax tree (AST).
As far as I can tell, this appears to be how Applesoft BASIC works too; for example, the following line runs fine:
GOTO 10BLARGBLARGO#@$OWEJR
If Applesoft had parsed the code into an AST before evaluating it, it would have raised a syntax error. Instead, it seems to be seeing the
GOTO 10and immediately moving to line 10, ignoring the rest of the line.ABASIC works in a similar way. I'm guessing that in Applesoft's case it was done to preserve memory; in ABASIC's case, it was mostly done for expediency, though I also liked the idea of preserving the behavior of first-generation BASIC interpreters.
That said, when loading input from a file, ABASIC performs static analysis, and will error if it notices anything out of the ordinary, such as a type mismatch, a
GOTOto an undefined line number, or the line shown above. This can be disabled with the--skip-checkflag. -
ABASIC has the optional ability to warn users about suspect code, such as when a variable that has never been assigned to is used in an expression (in such cases the variable defaults to zero or an empty string, as per BASIC's traditional behavior).
This feature can be enabled via the
-wflag on the command-line. -
The core implementation of ABASIC (contained in the
abasic-corecrate) was designed to have minimal dependencies and never block program execution for any reason (e.g. accepting user input). This allows it to be ported to the widest variety of platforms, including ones in which it may need to run on a UI thread that can never block (e.g. WASM).I had considered using Rust's
async/awaitfunctionality to achieve this, but it felt like overkill, so I opted for a "turn-taking" approach instead, where the client asks the interpreter to evaluate one statement at a time, and the interpreter lets it know if it's in a state where it needs additional data (e.g., user input).This effectively means that BASIC statements can appear to "block" for any reason, as their execution can be suspended at any statement boundary, but BASIC expressions cannot, as the recursive descent parser has no way of ceding control to the client without unwinding its stack.
However, this seems to be compatible with the behavior of most BASIC interpreters I've seen, so it doesn't seem to be a problem in practice.
Everything in this repository not expressly attributed to other sources is licensed under CC0 1.0 Universal (public domain).