An interpreter for QBasic, written in Rust.
The primary goal is to have an interpreter compatible with QBasic.
- Be able to interpret the sample
basic Docker project
- Be able to interpret
MONEY.BAS(an original demo program from QBasic)
- Unit tests for QBasic programs, with code coverage
- VS Code debugging
Tip: run tests continuously with
make watch or
nodemon -e rs -x "cargo test".
- Instruction generation
- Instruction interpretation
A program is read from a file character by character.
input (file or str) -> CharReader -> EolReader -> Parser
- CharReader returns one character a time, working with a
BufReadas its source.
- EolReader adds support for row-col position, handling new lines.
- Parsing is done with parser combinators, ending up in a parse tree of declarations, statements, expressions, etc.
The next layer is linting, where the parse tree is transformed into a different tree. In the resulting tree, all types are resolved. Built-in functions and subs are identified.
The instruction generator converts the linted parser tree into a flat list of instructions (similar to assembly instructions).
This is the runtime step where the program is being run, interpreted one instruction at a time.
Bare and qualified names
In QBasic, you can have a simple variable like this
A = 42.
You can also specify its type like this
A$ = "Hello, world!".
In rusty-basic, the first style is called bare name and the second style is called qualified name. The character that denotes the type is called a type qualifier.
There are five of these characters, matching the five built-in types:
Bare names also have a type. By default, it's single. So typing
will point to the same variable.
The default type can be changed to integer with the
DEFINT A-Z statement.
This simple name resolution mechanism gets a bit more complicated with the
For the lack of a better name, rusty-basic calls these variables extended:
DIM A AS INTEGER
DIM A AS SomeUserDefinedType
FUNCTION Add(A AS INTEGER, B AS INTEGER)
- cannot have a type qualifier (i.e. you can't say
DIM A$ AS INTEGER)
- when in scope, you can't have any other qualified name of the same bare name
So it's possible to have this:
A = 42 ' this is a single by default name resolution A$ = "hello"
But not this:
DIM A AS INTEGER A = 42 ' this is an integer because it's explicitly defined as such A$ = "hello" ' duplicate definition error here