AlexScript BASIC Interpreter
June 27, 2026 · View on GitHub
An Altair-style BASIC interpreter written in AlexScript. Inspired by the Ruby BASIC tutorial, ported to AlexScript language and extended into a more modular shape.
You write line-numbered BASIC programs at the prompt, type RUN, and
the interpreter executes them top to bottom (or wherever GOTO and
GOSUB send you). Programs can be saved to disk, loaded back, edited
line by line — the usual Altair vibe.
Files
basic.as entry point — start the REPL
repl.as interactive shell, command dispatcher, SAVE/LOAD
interpreter.as statement execution (every BASIC statement lives here)
parser.as expression parser, built-in functions, FN call dispatch
lexer.as tokenizer
runtime.as shared interpreter state (program buffer, variables,
arrays, FOR/GOSUB stacks, DATA pool, flags)
The Runtime class holds all mutable state — there are no globals
floating around between modules. Every other component takes a
Runtime instance and operates on it.
Running
From the directory containing the files:
alexscript basic.as
You should see:
AlexScript Altair Basic 0.0.1
Type 'HELP' for available commands
>
REPL Commands
| Command | What it does |
|---|---|
RUN | execute the stored program |
LIST | display the stored program |
NEW | clear the stored program |
CLEAR | clear the screen |
SAVE "file" | save program to a text file |
LOAD "file" | load program from a text file |
HELP | show the command list |
QUIT | exit |
A line that starts with a number is stored in the program buffer. Typing just the number deletes that line. Anything else is treated as an immediate command and executed on the spot.
Statements
PRINT
Comma-separated values get a space between them. A trailing semicolon suppresses the newline. Strings can be mixed with expressions freely.
PRINT "Hello, world!"
PRINT "X =", X, "Y =", Y
PRINT "no newline";
TAB(n) inside a PRINT jumps to column n.
LET (and implicit assignment)
LET X = 42
LET NAME$ = "Alice"
X = X + 1 ' LET is optional once X is defined
Array elements work the same way:
LET A(3) = 99
A(I, J) = I * J
INPUT
Prompt is optional. Multiple variables can be read from one line of input by separating them with commas.
INPUT N
INPUT "Enter your name: "; NAME$
INPUT "Coordinates: "; X, Y
$-suffixed names get the raw string, everything else is parsed as a
number (with 0 as fallback if parsing fails). Array targets are
supported too: INPUT A(I).
GOTO and IF / THEN / ELSE
IF X > 10 THEN PRINT "big" ELSE PRINT "small"
IF READY = 1 THEN GOTO 200
GOTO 50
The target of GOTO can be any expression that evaluates to a line
number, not just a literal.
FOR / NEXT
Counter variable, end value, and optional STEP. Works for nested
loops — NEXT without a variable closes the innermost loop.
FOR I = 1 TO 10
PRINT I
NEXT I
FOR X = 10 TO 0 STEP -2
PRINT X
NEXT X
GOSUB / RETURN / END
GOSUB pushes the next line onto a return stack and jumps; RETURN
pops it. END terminates the program.
10 GOSUB 100
20 PRINT "back"
30 END
100 PRINT "subroutine"
110 RETURN
DIM
Declares an array. Indices are 0-based and inclusive — DIM A(10)
gives you 11 slots from A(0) to A(10). Multi-dimensional arrays
are supported.
DIM A(10)
DIM GRID(5, 5)
DEF FN
User-defined one-liner functions. Parameters shadow same-named globals during the call and are restored afterwards. Functions can call other functions in their body.
DEF FN SQUARE(X) = X * X
DEF FN HYP(A, B) = SQR(FN SQUARE(A) + FN SQUARE(B))
PRINT FN HYP(3, 4) ' prints 5
DATA / READ / RESTORE
DATA declarations are collected into one big pool during a pre-pass
before the program runs, regardless of where they appear in the
source. READ consumes from the pool sequentially; RESTORE rewinds
the pointer back to the start.
10 DATA "Alice", 30, "Bob", 25
20 READ NAME$, AGE
30 PRINT NAME$, AGE
40 READ NAME$, AGE
50 PRINT NAME$, AGE
60 RESTORE
70 READ NAME$
80 PRINT NAME$ ' Alice again
REM and :
REM is a comment — works on its own line or as a trailing
annotation. Multiple statements can share a line by separating them
with colons.
10 REM this is a header comment
20 LET X = 5 : REM initialise
30 PRINT X : PRINT X*2 : PRINT X*3
Expressions
Operator Precedence (highest to lowest)
- parentheses, unary
-,NOT,~ ^(exponent, right-associative)*/+-=<><><=>=&,AND|,OR
Note:
&/|/AND/ORare treated uniformly as logical operators, not bitwise. This is a deliberate simplification from the Ruby original.
Built-in Functions
Math: ABS(x), SGN(x), SQR(x), INT(x), LOG(x), EXP(x),
SIN(x), COS(x), TAN(x), ATN(x), RND (random float in [0,1))
Strings: LEN(s), STR$(n), CHR$(code), VAL(s), LEFT$(s, n),
RIGHT$(s, n), MID$(s, start, length) — note MID$ is 1-indexed
the way classic BASIC does it.
Identifiers
Uppercase letters followed by uppercase letters or digits, with an
optional type suffix $, %, #, or !. Examples: X, A1,
NAME$, COUNT%. The suffix is part of the name, so A and A$
are different variables.
Example Programs
Factorial
10 PRINT "Calculate factorial"
20 INPUT "Enter a number: "; N
30 LET FACT = 1
40 FOR I = 1 TO N
50 LET FACT = FACT * I
60 NEXT I
70 PRINT N; "! ="; FACT
> RUN
Calculate factorial
Enter a number: ? 5
5! = 120
Subroutines
10 LET N = 5
20 GOSUB 100
30 PRINT N; "! ="; F
40 LET N = 7
50 GOSUB 100
60 PRINT N; "! ="; F
70 END
100 LET F = 1
110 FOR I = 1 TO N
120 LET F = F * I
130 NEXT I
140 RETURN
> RUN
5! = 120
7! = 5040
Data and Loops with an Array
10 DATA "Alice", 30, 165
20 DATA "Bob", 25, 175
30 DATA "Carol", 35, 160
40 LET TOTALAGE = 0
50 FOR I = 1 TO 3
60 READ NAME$, AGE, HEIGHT
70 PRINT "NAME:"; NAME$; " AGE:"; AGE; " HEIGHT:"; HEIGHT
80 LET TOTALAGE = TOTALAGE + AGE
90 NEXT I
100 PRINT "Average age ="; TOTALAGE / 3
> RUN
NAME: Alice AGE: 30 HEIGHT: 165
NAME: Bob AGE: 25 HEIGHT: 175
NAME: Carol AGE: 35 HEIGHT: 160
Average age = 30
User-Defined Functions
10 DEF FN SQUARE(X) = X * X
20 DEF FN CUBE(X) = X * X * X
30 FOR X = 1 TO 5
40 PRINT "x="; X; " sq="; FN SQUARE(X); " cube="; FN CUBE(X)
50 NEXT X
> RUN
x=1 sq=1 cube=1
x=2 sq=4 cube=8
x=3 sq=9 cube=27
x=4 sq=16 cube=64
x=5 sq=25 cube=125
Architecture Notes
Runtimeclass instead of globals. A single instance is threaded through the interpreter and parser. Easier to reason about, no shared mutable state across files.- Sorted line index. A parallel sorted array of integer line
numbers lives next to the program buffer, so
RUNandGOTOdon't pay for a re-sort on every iteration. - Index-based lexer. The original Ruby version mutates a
character array via
shift. This port keeps apositioncursor on the source string — same semantics, no per-token allocation. assign_to_targetshared byLETandINPUT. Whether the target is a scalar, an array element, or a multi-dim slot, both statements go through one assignment routine. Bug fixes on either side benefit both.- Two-pass program execution. First pass scans for
DATAdeclarations and builds the data pool; second pass runs the program normally.DATAlines are no-ops during normal flow.