Language Reference - CRT Basic

Table of Contents

Back to main documentation

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 .bas files.
  • Comments: ' anywhere, or REM as 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 for GOTO, GOSUB and ON 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:

  • x and x! 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.2 keeps full precision during the addition, then narrows to SINGLE when assigned.
  • DIM name AS INTEGER fixes a suffix-less name's type the same way a suffix would.
  • LET is accepted as an explicit-assignment keyword (LET x = 1 is plain assignment, including LET 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 DIM is auto-dimensioned 0 TO 10 per 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 = 42 and xo(3) = 7 use independent storage and never disturb each other (QB45 behavior, oracle-verified).
  • ERASE name frees 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 = getChar calls getChar(), unless a variable by that name exists).
  • DECLARE statements carry parameter type suffixes: when a SUB/FUNCTION header uses bare names, the declared suffixes type the parameters.
  • A parameter declared AS type claims its bare name inside the body: under DEFINT A-Z, SUB d (x AS SINGLE) makes every bare x refer to the SINGLE parameter. A suffix parameter does not: in SUB p (a$), a bare a is 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; \ and MOD round 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

Numbers print the way classic BASIC formats them:

  • A leading space for non-negative values and a trailing space — PRINT 1 emits " 1 ".
  • SINGLE values print with float32's ~7 significant digits; DOUBLE expressions (a #-suffixed literal or variable anywhere inside) print with 16 significant digits, using D±xx scientific 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.

Created: 2026-08-31 ma 01:26

Validate