Language Reference - CRT Basic
Table of Contents
1. The BASIC dialect
CRT Basic implements the structured BASIC dialect common to early-90s
DOS programs: optional line numbers, named procedures, typed variables
by suffix, and the classic SCREEN graphics environment.
This page documents what the interpreter currently supports. The project is under active development: unsupported statements fail with an error naming the statement and the source line, and support grows program by program.
2. Source files
- Files are read as CP437 with CRLF tolerated — the natural format
of DOS-era
.basfiles. - Comments:
'anywhere, orREMas a statement. Both run to end of line. - Statement separators: newline or
:. Multiple statements per line work (A = 1 : B = 2). - Line continuation: a
_as the last non-blank character of a line joins it with the next. - Case: keywords and variable names are case-insensitive.
- Numbers: decimal (
3.25), exponents (1.5e3,2D+10), hex&HFF, octal&O17, and type-suffixed literals (1024&,1.5#). - Labels: numeric (
1 IF x THEN GOTO 2) or alphanumeric (MainLoop:). Targets forGOTO,GOSUBandON ERROR GOTO.
3. Variables and types
A variable's type comes from its name suffix:
| Suffix | Type | Storage |
|---|---|---|
% |
INTEGER | 16-bit integer, rounded half-to-even on store; out of range → Overflow |
& |
LONG | 32-bit integer, same rounding/overflow rules |
! |
SINGLE | 32-bit float (narrowed on store) |
# |
DOUBLE | 64-bit float |
$ |
STRING | dynamically sized string |
A name without a suffix gets a default type: SINGLE, unless changed
by DEFINT / DEFLNG / DEFSNG / DEFDBL / DEFSTR letter ranges
(DEFINT A-Z makes every plain name an INTEGER). The default applies
by first letter and is module-wide, independent of where the DEFxxx
statement appears.
Important consequences:
xandx!are the same variable (under the default rules);x%is a different one.- Numbers are computed in double precision inside expressions and
narrowed to the variable's type on store.
y = 0.1 + 0.2keeps full precision during the addition, then narrows to SINGLE when assigned. DIM name AS INTEGERfixes a suffix-less name's type the same way a suffix would.LETis accepted as an explicit-assignment keyword (LET x = 1is plain assignment, includingLET a(i) = v).CONST name = expr [, …]defines module-level named constants (may reference earlier constants); reassigning one raises Duplicate definition.- Unassigned numeric variables read as
0, strings as"".
Truth values: comparisons return -1 for true and 0 for false.
Any nonzero number is true in IF / WHILE conditions. AND, OR
and NOT are bitwise operations on integers, exactly as classic
BASIC defines them.
4. Arrays
DIM scores(0 TO 50) ' explicit bounds DIM grid(10, 20) ' 0-based, two dimensions DIM SHARED high%(9) ' visible inside SUBs DIM names$(20) AS STRING ' AS type form
- An array used without
DIMis auto-dimensioned0 TO 10per dimension on first indexed use — the classic default. - Indexing out of range raises Subscript out of range.
- A scalar and an array may share one name:
xo = 42andxo(3) = 7use independent storage and never disturb each other (QB45 behavior, oracle-verified). ERASE namefrees an array; it can be re-=DIM=med afterwards.- Element types follow the same suffix rules as scalars; INTEGER arrays narrow and range-check each stored element.
5. Statements
5.1. Screen and graphics
| Statement | Notes |
|---|---|
SCREEN mode, SCREEN m,,,apage,vpage |
Modes 0, 1, 2, 7–13; switches resolution, palette, page count; default foreground becomes the mode's highest attribute (1/3/15), except SCREEN 13 where real QBasic defaults to 15; background black |
PSET (x, y)[, color] |
One pixel; default color = current text foreground |
POINT(x, y) |
Function: palette index of a pixel |
LINE (x1,y1)-(x2,y2)[, color] |
Pixel-exact DDA line, clipped like the original; start point may be omitted (LINE -(x2,y2) draws from the last point referenced) |
LINE …, , B / BF |
Rectangle outline / filled rectangle (inclusive corners) |
CIRCLE (x,y), r[, color][,,,aspect] |
Ellipse via aspect; arcs (start/end angles) not yet |
PAINT (x,y)[, fill[, border]] |
Flood fill: paints over any color up to the border (border defaults to fill) |
GET (x1,y1)-(x2,y2), array |
Captures a screen rectangle into a variable |
PUT (x,y), array[, action] |
Blits a captured rectangle back; action is PSET, XOR, OR or AND |
PCOPY source, target |
Copies one video page onto another |
PALETTE via OUT &H3C8/&H3C9 |
DAC palette ports are modeled; other ports ignored |
5.2. Text
| Statement | Notes |
|---|---|
PRINT expr [; or ,] … |
; compact, , next 14-column print zone; trailing separator suppresses the newline |
LOCATE row[, col] |
1-based cursor position; omitted col keeps the current column |
COLOR fg[, bg] |
Text colors (palette indices) |
WIDTH cols[, rows] |
Text grid: 80x25 (default), 40x25 or 80x50 in SCREEN 0; 40 columns render double-wide glyphs, 50 rows swap to the 8x8 font; clears the screen |
CLS |
Clears to background, homes cursor |
VIEW PRINT top TO bottom |
Scroll region: printing and scrolling confined to the band; bare VIEW PRINT restores the full screen |
CSRLIN, POS(0) |
Functions: cursor row / column |
5.3. Control flow
| Statement | Notes |
|---|---|
FOR v = a TO b [STEP s] … NEXT |
STEP 0 raises an error |
IF c THEN … [ELSEIF c THEN …] [ELSE …] |
Single-line and block (END IF / ENDIF) forms; ELSEIF in block form only; THEN may be omitted before GOTO=/=GOSUB (IF c GOTO label) |
WHILE c … WEND |
|
DO [WHILE/UNTIL c] … LOOP [WHILE/UNTIL c] |
Condition on either end |
SELECT CASE x … CASE a, b … CASE ELSE … END SELECT |
Equality cases (CASE IS, CASE a TO b not yet) |
GOTO label |
Procedure-wide label scope; may jump INTO a label nested inside IF/FOR/DO bodies (headers are not re-evaluated, loops resume with current variable values) |
GOSUB label / RETURN |
Nested calls OK; the label may live in an enclosing block of the same procedure — RETURN rebuilds the nested origin and loops keep iterating. Not supported: GOSUB to a label nested inside a block statement |
END, SYSTEM |
Both end the program |
5.4. Procedures
DECLARE SUB DrawStar (cx%, cy%, r!)
CALL DrawStar(100, 100, 20)
DrawStar 50, 50, 10 ' bare-call form, no parentheses
SUB DrawStar (cx%, cy%, r!)
...
END SUB
FUNCTION Area# (r!)
Area# = 3.141592653589793# * r! * r!
END FUNCTION
- Arguments that are plain variables are passed by reference: assignments to the parameter write through to the caller's variable. Expressions are passed by value.
- A FUNCTION's return value is whatever was last assigned to the function's own name inside the body.
- A parameterless FUNCTION may be called without parentheses (
x = getCharcallsgetChar(), unless a variable by that name exists). DECLAREstatements carry parameter type suffixes: when a SUB/FUNCTION header uses bare names, the declared suffixes type the parameters.- A parameter declared
AS typeclaims its bare name inside the body: underDEFINT A-Z,SUB d (x AS SINGLE)makes every barexrefer to the SINGLE parameter. A suffix parameter does not: inSUB p (a$), a bareais a separate DEFxxx-typed variable. - A bare call's first argument may be parenthesized:
prn (x + 1) * 2, 10, a$parses as three arguments, the parens are plain grouping. - SUBs do not see the caller's variables (classic scoping), except
names declared
DIM SHARED.
5.5. Data, files, errors, misc
| Statement | Notes | |
|---|---|---|
DATA v, … / READ var, … |
DATA is module-wide, collected in program order; running out raises Out of DATA | |
RANDOMIZE [seed] / RND[(n)] |
Bit-exact generator; no seed = RANDOMIZE TIMER |
|
OPEN path FOR INPUT AS #n |
Text files; DOS paths resolve relative to the .bas file (or the CHDIR state): backslash separators and case-insensitive components like the DOS filesystem |
|
OPEN path FOR OUTPUT/APPEND AS #n, PRINT #n, ... |
Writing; OUTPUT truncates, APPEND adds at end; PRINT # uses console formatting with CRLF line endings |
|
CHDIR path$, MKDIR path$ |
Current-directory state and directory creation; MKDIR on an existing directory raises Path/file access error like real QBasic |
|
INPUT #n, var, … |
Numbers delimited by comma, space or newline; strings by comma/newline (quotes stripped); empty slots read as 0 / "" |
|
| =INPUT [;]["prompt"{; | ,}] var, …= | Console line input: prompt echoed ("; " adds ? =), typed line echo with Backspace editing, items comma-delimited like =INPUT # |
LINE INPUT #n, var$ |
Whole line | |
EOF(n) |
True at end of channel | |
CLOSE [#n] |
Bare form closes all | |
ON ERROR GOTO label / ON ERROR GOTO 0 |
Enable / disable trapping | |
RESUME, RESUME NEXT, RESUME label |
Retry, continue, or jump | |
SOUND freq, duration |
Audible square wave (PC-speaker timbre) in GUI mode, one-note queue at 18.2 ticks/s; timing-only when headless or no audio device | |
BEEP |
Desugars to SOUND 800, 4.55 (the classic ~1/4 s beep) |
|
SWAP a, b |
Exchanges two variables or array elements; string/number mixes raise Type mismatch | |
OUT port, value |
VGA DAC ports only; others ignored | |
DEF SEG = segment |
Selects the segment PEEK / POKE operate on; bare DEF SEG resets to 0 |
|
POKE addr, value |
Writes the low byte of value at DEF SEG:addr in the emulated machine RAM |
6. Expressions and operators
Precedence, weakest first:
| Level | Operators |
|---|---|
| 1 | OR |
| 2 | AND |
| 3 | NOT (unary) |
| 4 | = = <> < <= > >= = (comparison) |
| 5 | =+ - = |
| 6 | * / \ MOD |
| 7 | unary =- = |
| 8 | ^ |
Semantics worth knowing:
/is floating-point division;\andMODround both operands to integers (half-to-even) first, then divide / take the remainder.- Division by zero raises Division by zero.
+on two strings concatenates.- Comparison of strings is by exact/lexicographic string order; comparing a string to a number raises Type mismatch.
7. Built-in functions
| Function | Result |
|---|---|
ABS, SGN, INT, FIX |
magnitude/sign/floor/truncation |
CINT, CLNG |
round half-to-even into int16 / int32 range (Overflow if out of range) |
SQR, EXP, LOG |
square root, e^x, natural log |
SIN, COS, TAN, ATN |
radians, double precision |
RND[(n)] |
n>0 or omitted: next number; n=0: repeat last; n<0: reseed first |
TIMER |
seconds since local midnight (single precision) |
TIME$ |
current time, "HH:MM:SS" |
POINT(x, y) |
palette index of a pixel |
POS(0), CSRLIN |
cursor column / row (1-based) |
CHR$(n), ASC(s$) |
character ↔ code |
STR$(n), VAL(s$) |
number ↔ string; VAL parses the longest numeric prefix (junk tail ignored, junk start is 0) and understands &H… / &O… with the literal's signed semantics (VAL("&HFFFD") is -3) |
HEX$(n) |
uppercase hex, no prefix; INTEGER-range negatives print as 16-bit two's complement (-3 becomes ="FFFD") |
PEEK(addr) |
byte at DEF SEG:addr in the emulated machine RAM |
LEN, LEFT$, RIGHT$, MID$ |
string length and slices (1-based) |
UCASE$, LCASE$ |
case conversion |
LTRIM$, RTRIM$ |
strip spaces |
SPACE$(n) |
string of n spaces |
INKEY$ |
one key, non-blocking; "" when idle |
INPUT$(n) |
read up to n input bytes (extended keys arrive as CHR$(0) + scancode) |
EOF(n) |
-1 at end of file channel |
8. PRINT formatting
Numbers print the way classic BASIC formats them:
- A leading space for non-negative values and a trailing space —
PRINT 1emits" 1 ". - SINGLE values print with float32's ~7 significant digits; DOUBLE
expressions (a
#-suffixed literal or variable anywhere inside) print with 16 significant digits, usingD±xxscientific notation outside the fixed range. ,advances to the next 14-column print zone;;prints compactly. A trailing separator suppresses the newline.
Whole glyph cells are painted when text lands in a graphics mode — see Text is pixels for why that matters.
9. Keyboard input model
Programs read keys through INKEY$ (single key, non-blocking) and
INPUT$(n) (up to n bytes). Printable keys arrive as their character
(Enter as CHR$(13)). Cursor keys, function keys and editing keys
arrive as the classic two-byte sequence CHR$(0) followed by the
scancode — INKEY$ returns both bytes in one call, INPUT$ hands
them out across successive reads.
| Key | Sequence |
|---|---|
| Up / Down / Left / Right | CHR$(0) + 72 / 80 / 75 / 77 |
| Home / End / PgUp / PgDn | CHR$(0) + 71 / 79 / 73 / 81 |
| Insert / Delete | CHR$(0) + 82 / 83 |
| F1–F10 | CHR$(0) + 59–68 |
| Esc, Enter, Backspace, Tab | single byte: 27, 13, 8, 9 |
In headless mode input never blocks: both functions simply drain
whatever a driver pushed into the keyboard queue and return "" when
it is empty.
10. Machine memory and the emulated mouse driver
DEF SEG, PEEK and POKE operate on an emulated 1 MiB real-mode
address space (physical address = segment * 16 + offset, wrapping at
20 bits). Most of it is plain RAM; one region is live.
CRT Basic behaves as if the qbext mouse TSR (qbasicapps "Mouse
driver") is always loaded: the IVT slot of INT 79h holds a far pointer
to an 8-byte data table, and mouse events from the window frontend (or
a headless driver) accumulate into it. Programs that speak the TSR
protocol — discover the pointer with DEF SEG = 0 and =PEEK(&H79 * 4
- n)=, verify the magic word 1983, then read-and-reset the fields —
work unmodified.
Table layout (little-endian words):
| Offset | Contents |
|---|---|
0x00 |
magic number 1983 |
0x02 |
horizontal movement, accumulated |
0x04 |
vertical movement, accumulated |
0x06 |
button status, latched: bit 0 left, 1 right, 2 middle |
Like the real driver, movement deltas wrap at 16 bits (read them as
signed, e.g. via VAL("&H" + HEX$(...))), button bits stay latched
until the consumer POKEs the field back to 0, and a field reading 0
means "no new activity". A button held down re-arms its bit after
every consumer reset for the whole depression — the real TSR polls the
live button state (INT 33h function 03h) on every timer tick, so a
program that reads-and-resets each frame sees the bit set on every
frame until the physical release (plus one stale latched read). In
the window frontend, mouse motion is converted to video-mode pixels
before being accumulated, so movement feels 1:1 at any window size.
11. Error behavior
Runtime errors are fatal by default and always name the source line
(plus the offending source text when available). Trap them with
ON ERROR GOTO label; RESUME retries the trapped statement,
RESUME NEXT continues after it, RESUME label jumps. ON ERROR
GOTO 0 disables trapping.
Common runtime errors: Overflow (integer store out of range), Division by zero, Subscript out of range, Type mismatch, Out of DATA, Input past end of file, File not found, Bad file number, Label not found.