Architecture - CRT Basic
Table of Contents
1. Architecture
CRT Basic is a classic language pipeline feeding a set of virtual
devices. A .bas file is tokenized, parsed into an AST, and executed
against a virtual VGA card, a keyboard queue and a sound timer.
The guiding idea is that devices are shared between frontends. The
interactive Swing window and the headless agent drivers talk to the
exact same VgaDevice and KeyboardQueue instances; only the outer
shell differs. A program therefore behaves identically whether a
human watches it in a window or an agent samples it offscreen.
2. Package map
| Package | Key classes | Responsibility |
|---|---|---|
| (root) | Main |
CLI entry point: interactive vs headless modes |
lexer |
Lexer, Token |
Source text → token stream |
parser |
Parser |
Tokens → AST, recursive descent |
ast |
Ast (sealed Stmt / Expr records) |
Typed syntax tree, every node carries its source line |
interp |
Interpreter |
Executes the AST (interim tree-walker) |
video |
VgaDevice, Framebuffer, TextConsole, ScreenMode, VgaFont, DefaultPalettes, CapturedRegion |
The virtual VGA card |
input |
KeyboardQueue |
Single input path for all frontends |
audio |
SoundQueue |
SOUND timing model (no audio yet) |
frontend |
SwingFrontend |
Interactive window: live screen + key capture |
Test sources additionally carry the JUnit suites, the
VideoDemoMain showcase renderer and the GameDriver play-testing
driver (see Agent integration).
3. The pipeline, stage by stage
3.1. Lexer (lexer)
Converts source text into tokens. The lexer is deliberately tolerant
of what real DOS-era .bas files look like:
- Files are read as CP437 (IBM437) with CRLF line endings — the
charset is applied at file-read time in
Main. - Both newlines and
:are statement separators (SEPtokens). 'comments and word-boundary-matchedREMcomments are dropped.- Numbers: decimal with optional fraction and
E=/=Dexponents (1.5e3,2D+10), hex&H…, octal&O…, and trailing type suffixes (1024&,1.5#,2!). - Identifiers may carry the BASIC type suffixes
% & ! # $and may contain.and_; CP437 high characters pass through in strings and identifiers. - A
_as the last non-blank character of a line joins physical lines into one logical statement (line continuation).
Keywords are not special at this stage: they are emitted as plain identifiers and the parser matches them by text, case-insensitively. This keeps the lexer small and makes the grammar easy to extend.
3.2. Parser (parser, ast)
A hand-written recursive-descent parser builds the AST. Every node is
an immutable record (sealed Ast.Stmt / Ast.Expr interfaces) that
carries the 1-based source line, so runtime errors can quote the
failing source line.
Statements that are not (yet) supported throw a ParseException
naming the statement and the line. This is a feature: when a new
vintage program fails to load, the error message is the work item.
Expression parsing follows the usual BASIC precedence (weakest to
strongest): OR, AND, NOT, comparison (= = <> < <= > >= ),
addition, multiplication ( * / \ MOD ), unary minus, power (=^),
primaries.
3.3. Interpreter (interp)
Executes the AST directly (tree-walking). This is the interim execution engine: the target architecture compiles the AST to a compact stack-based bytecode and runs it on a small VM. The tree-walker exists so that whole programs run today and the verification workflow applies at program level; lexer, parser and AST carry over to the bytecode world unchanged.
The interpreter is where BASIC semantics live: float32 narrowing on
store, type suffixes, true = -1, classic number formatting in
PRINT, by-reference SUB parameters, module-wide DATA, the 24-bit
RND generator. All of that is documented on the
Interpreter semantics page.
3.4. Video (video)
A virtual VGA card: indexed-color framebuffer with multiple video
pages, a text console that blits CP437 glyphs into the pixel buffer,
mode table for SCREEN 0–13, default palettes, drawing primitives
(PSET, LINE, CIRCLE, PAINT, GET=/=PUT) and the DAC palette
ports reachable through OUT. Everything the program can see is
recoverable: PNG screenshots of the visual page, or the raw indexed
pixels of every page. Full details: Video subsystem.
3.5. Input (input)
KeyboardQueue is a thread-safe blocking queue of key presses: a
KeyEvent VK code plus the typed character where one exists. Any
driver pushes keys in — the Swing window, a JUnit test, an agent
script — and the program consumes them through INKEY$ / INPUT$.
The mapping from modern key codes to the BASIC byte sequences
(printable character, or CHR$(0) + scancode for extended keys)
happens at consumption time inside the interpreter.
3.6. Audio (audio)
SoundQueue models the timing of SOUND without producing audio:
durations are in PC timer ticks (18.2 per second), the note queue is
one note deep, and the next SOUND blocks until the previous note
finishes. This preserves two classic idioms — SOUND 0, n as a delay
and a single short SOUND per loop iteration as a frame-rate
limiter — which is what game programs actually depend on.
3.7. Frontend (frontend)
SwingFrontend is deliberately dumb: it paints the visual page of
the VgaDevice into a window (nearest-neighbor scaling, ~30 fps
repaint timer) and pushes key presses into the KeyboardQueue. It
owns no emulation state. Headless operation simply skips this class.
4. Entry point: two ways to run
Main wires the pieces together and selects one of two modes:
crtbasic program.bas interactive
crtbasic program.bas --headless [--screenshot=out.png]
[--raw=out.raw] [--steps=N] agent path
- Interactive (default): the Swing window is shown and the program
runs on a daemon thread; key presses feed the input queue and
INPUT$blocks waiting for keys. - Headless: no window.
--steps=Ncaps executed statements so endless game loops can be sampled; afterwards the framebuffer is dumped as PNG (--screenshot) and/or raw indexed pixels (--raw, all video pages concatenated). Input never blocks —INKEY$/INPUT$just drain whatever a driver queued.
Relative paths in OPEN resolve against the directory of the .bas
file, so programs find their data files regardless of the caller's
working directory.
Startup errors (unreadable file, parse error) go to stderr in headless mode and to an error dialog in interactive mode, where nobody would see stderr.
5. Design decisions
5.1. Bytecode VM is the target; the tree-walker is scaffolding
SCREEN 13 programs sit in tight pixel loops. A compact stack-based
bytecode keeps interpretation overhead low and gives a natural
"execute N instructions" granularity for deterministic stepping. The
tree-walking interpreter is a deliberate interim: it made whole
programs runnable (and verifiable) from day one. When the compiler
and VM land, only interp is replaced.
5.2. Introspection is a feature, not an afterthought
The interpreter exposes the executed-statement counter, variable
values from the global scope, and the raw framebuffer. An agent can
therefore assert on program state — "after 1000 statements,
score% must be 3 and pixel (10,10) must be color 4" — instead of
guessing from prose output.
5.3. Fail loudly on the unknown
Unsupported statements raise errors that name the statement and the source line. Silent misinterpretation would poison debugging and verification: a wrong behavior must mean a wrong implementation, never a skipped feature.
5.4. One device set, many frontends
Because frontends share VgaDevice and KeyboardQueue, there is no
"headless build" and no behavioral drift between interactive and
automated runs. The --headless flag only skips window creation and
input blocking.
6. Repository layout
crtbasic/
├── README.org project overview and status
├── TODO.org work items (games to support, features)
├── COPYING license
├── pom.xml Maven build (Java 21, jar with Main-Class)
├── install.sh builds and installs to ~/.local
├── Documentation/ this documentation
└── src/
├── main/java/eu/svjatoslav/crtbasic/
│ ├── Main.java
│ ├── lexer/ parser/ ast/ interp/
│ ├── video/ input/ audio/ frontend/
│ └── resources/fonts/ CP437 bitmap fonts (8x8, 8x14, 8x16)
└── test/java/eu/svjatoslav/crtbasic/
├── video/ unit tests + VideoDemoMain showcase
└── drivers/ GameDriver (scripted play-testing)
The CP437 font binaries under resources/fonts were extracted from
the public-domain VGA fonts shipped with SeaBIOS
(vgasrc/vgafonts.c): 256 glyphs each, one byte per glyph scanline.