Coder Social home page Coder Social logo

rusty-basic's Introduction

rusty-basic

An interpreter for QBasic, written in Rust.

Goals

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

Development

Tip: run tests continuously with make watch or nodemon -e rs -x "cargo test".

Architecture

  • Parsing
  • Linting
  • Instruction generation
  • Instruction interpretation

Parsing

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 BufRead as 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.

Linting

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.

Instruction generation

The instruction generator converts the linted parser tree into a flat list of instructions (similar to assembly instructions).

Instruction interpretation

This is the runtime step where the program is being run, interpreted one instruction at a time.

Names

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:

  • % 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.

Extended names

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)

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 here

rusty-basic's People

Contributors

ngeor avatar renovate-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

rusty-basic's Issues

Support DIM SHARED

  • Gives subprograms access to module (global) variables
  • Only allowed on module level, throws an error if used on sub/fn level
  • Cannot DIM a variable by the same name in a sub/fn level

Support DIM keyword for built-in types

e.g.

DIM X AS STRING
X = "hello"
PRINT X

should print hello (even though X is not marked with the dollar sign).

X = 42 should yield a Type Mismatch error, as X is a string

Built-in types: Integer, Long, Single, Double, String

Support PRINT with comma and semicolon separators

PRINT with comma should print at the next print zone (14 characters wide).
PRINT with semicolon should print immediately after the previous expression.
It is possible to combine commas and semicolons in one PRINT statement.
Positive numbers have a leading space (possibly to align with the minus sign of negative numbers).
Numbers have a trailing space even when using semicolons.

Implement random access files

Example statements

OPEN file$ FOR RANDOM AS #1 LEN = 84
FIELD #1, 8 AS IoDate$, 10 AS IoRef$
GET #1, 1
LSET IoDate$ = ""
PUT #1, 2

For OPEN:

  • LEN = reclen%, only for random access files, the record length in bytes. The default is 128 bytes.
  • RANDOM is the default file mode

For FIELD:

FIELD [#]filenumber%, field-width% AS string-variable$ [, field-width% AS string-variable$ ] ...

For GET and PUT:

GET [#]filenumber% [, [record-number&][,variable] ]
PUT [#]filenumber% [, [record-number&][,variable] ]

For LSET and RSET:

LSET and RSET move data into a random-access file buffer (in preparation for a PUT statement) and left- or right- justify the value of a string variable. LSET also copies the contents of one record variable to another.

LSET string-variable$ = string-expression$
RSET string-variable$ = string-expression$
LSET record-variable1 = record-variable2

Support popping arguments in built-in functions to avoid clone

For example, in ubound.rs:

let v: Variant = interpreter.context()[0].clone()

can be done by popping the variant instead.

Note that this is more difficult due to the way ByRef arguments are implemented (they're expected to be in-place, so popping causes issues).

Implement DEF SEG

DEF SEG [=address]

Sets the current segment address.

Used by BLOAD, BSAVE, CALL ABSOLUTE, PEEK OR POKE. Address is a value in the range 0 to 65535. If omitted, resets the current segment.

Implement the following example:

DIM ScrollUpAsm(1 TO 7) ' notice 7 elements here
P = VARPTR(ScrollUpAsm(1))
DEF SEG = VARSEG(ScrollUpAsm(1))
FOR I = 0 TO 13 ' but 14 iterations here
    READ J
    POKE (P + I), J
NEXT I

Implement DATA statement

Implement DATA and READ statements

e.g.

DATA "hello", 42
READ A$, B%
PRINT A$, B%

Can throw runtime error "Out of DATA" (or type mismatch)

Implement WIDTH

e.g. WIDTH , 25

Assign an output-line width to a device or file

WIDTH [columns%] [,rows%]
WIDTH #filenumber% columns%
WIDTH LPRINT columns%

[Tech Debt] Unify or refactor errors

  • Each layer has its own error similar error class. See if we can reduce their number (e.g. ParseError vs RuntimeError).
  • Remove the Result<T> aliases, always use Result<T, E>

unit testing and code coverage

Functions that start with Test could be considered as unit tests and they can return a boolean condition to indicate pass or fail:

FUNCTION Add(a, b)
    Add = a + b
END FUNCTION

FUNCTION TestAdd
    TestAdd = Add(1, 2) = 3
END FUNCTION
basic-interpreter-rust PROGRAM.BAS --test --with-code-coverage

Implement DO LOOP

DO [ WHILE | UNTIL ] condition
    statements
LOOP

or

DO
    statements
LOOP [ WHILE | UNTIL ] condition

Pre-parser?

Consider adding a layer between the lexer and the parser. It would generate a flat list of items (not a parse tree) but a bit richer than the ones the lexer generates.

Example items:

  • Comment(String)
  • Operand(LessOrEqual)

Add a flag to indicate if running on Apache

When running in Apache with cgi-bin we have two hacks in place:

  • reading the program from the BLR_PROGRAM variable
  • setting the current directory to the program's current directory

These two hacks should be applied only when running in Apache.

Implement VIEW PRINT

Sets the boundaries of the screen text viewport

VIEW PRINT [toprow% TO bottomrow%]

If the arguments are omitted, the entire screen will be used as the text viewport.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.