An interpreter for QBasic, written in Rust.
The primary goal is to have an interpreter compatible with QBasic.
- Be able to interpret the sample TODO.BAS from my basic Docker project ✅
- Be able to interpret
MONEY.BAS(an original demo program from QBasic) ✅
Secondary goals:
- Be able to cross-compile to Rust and/or JavaScript
- Unit tests for QBasic programs, with code coverage
- VS Code debugging
- Parsing
- Linting
- Instruction generation
- Instruction interpretation
A program is read from a file character by character.
input (file or str) -> RcStringView -> Parser
The parser combinator framework is agnostic of the rest of the project
and lives in the rusty_pc package.
The input source is the RcStringView which is created by a file or string,
reading the entire contents in memory, and calculating the row/col for each
character (taking into account line endings of various platforms,
e.g. CRLF for Windows, LF for other platforms).
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.
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:
%for integer&for long!for single#for double$for string
Bare names also have a type. By default, it's single. So typing A and A!
will point to the same variable.
The default type can be changed to integer with the DEFINT A-Z statement.
There's also DEFLNG, DEFSNG, DEFDBL and DEFSTR.
This simple name resolution mechanism gets a bit more complicated with the AS
keyword.
For the lack of a better name, rusty-basic calls these variables extended:
DIM A AS INTEGERDIM A AS SomeUserDefinedTypeFUNCTION Add(A AS INTEGER, B AS INTEGER)
These names:
- 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 hereThe project uses the nightly toolchain currently,
to take advantage of rustfmt features that are onyl available there.
Additionally, building on a Mac might cause some linker errors on the current stable toolchain, which aren't occurring on the nightly.
Tip: run tests continuously with make watch or
nodemon -e rs -x "cargo test".