Interpreter Semantics - CRT Basic
Table of Contents
1. Execution engine
The Interpreter class executes the AST directly, tree-walking. It
is the interim engine: the target architecture compiles the AST to
a compact stack-based bytecode executed by a small VM, which is
cheaper in tight pixel loops and gives a natural "execute N
instructions" granularity. The tree-walker exists so that whole
programs run — and can be verified — today. The lexer, parser and
AST carry over unchanged; only this package gets replaced.
Because BASIC semantics are subtle and real programs depend on the subtleties, they are pinned down here explicitly.
2. The numeric model
- Expression evaluation always runs in double precision. Precision is only lost when a value is stored: into a variable, an array element, or a numeric literal narrower than DOUBLE.
- Storage narrows to the slot's type. SINGLE slots (the default) narrow to float32. INTEGER (= %=) and LONG (= &=) slots round half-to-even and raise Overflow when out of range. DOUBLE (= #=) slots keep everything.
- Numeric literals narrow to float32 unless written with a
#suffix or aDexponent, which mark them DOUBLE. - Graphics coordinates and colors convert fractional values by rounding half-to-even — not truncation.
In practice: a value computed as 0.1 + 0.2 keeps full double
precision through the addition and narrows exactly once, on
assignment.
3. Variable identity
Variable names are case-insensitive but suffix-sensitive:
xandx!are the same variable;x%is a different one.- A suffix-less name resolves its suffix from
DEFINTand siblings (module-wide, by first letter), or from aDIM name AS typedeclaration, defaulting to SINGLE. - Unassigned numeric variables read as
0, strings as"". - True is
-1, false is0;AND/OR/NOTare bitwise.
4. Procedures and scoping
- SUBs and FUNCTIONs register at load time and never execute inline; main code is everything else.
- Plain-variable arguments pass by reference — the parameter shares the caller's storage cell, so writes through the parameter are visible to the caller. Expression arguments pass by value.
- SUBs do not see the caller's other variables.
DIM SHAREDnames resolve against the global scope from inside any SUB. - When a SUB/FUNCTION header uses bare parameter names, the matching
DECLAREstatement's suffixes decide the parameter types. - A FUNCTION returns whatever was last assigned to its own name
inside the body (
0/""when never assigned). - Arrays not yet
DIM=med auto-dimension to =0 TO 10per dimension on first indexed use;ERASEreturns a slot to its never-DIMmed state.
5. Labels, GOTO and error handling
Each statement block executes under a program counter. A GOTO
throws a control-flow signal carrying the label; the enclosing block
catches it when it contains the label, and rethrows otherwise — which
gives labels procedure-wide scope, exactly as the language defines,
without any pre-pass.
ON ERROR GOTO registers a module-global handler label. A trapped
runtime error transfers control to the handler, remembering the
program counter of the failing statement. RESUME retries that
statement, RESUME NEXT continues after it, RESUME label jumps.
ON ERROR GOTO 0 disables trapping. (For an error raised inside a
SUB, the current approximation retries the CALL statement.)
DATA constants are module-wide: they are collected in program order
before execution begins, and READ walks that list. Running out
raises Out of DATA.
6. RND is bit-exact
The random generator reproduces the reference algorithm exactly: a 24-bit linear congruential generator
seed = (seed * &HFD43FD + &HC39EC3) mod 2^24 RND = seed / 2^24 (as SINGLE)
- Without
RANDOMIZE, the generator starts from the same fixed seed as the reference, so programs get the identical deterministic sequence. RND(0)repeats the last value;RND(-n)reseeds first;RANDOMIZE nreseeds likeRND(-n); bareRANDOMIZEseeds from the clock (TIMER).- Reseeding folds the SINGLE bit pattern of
|n|into the 24-bit seed as(bits & &HFFFFFF) + (bits >>> 24) + &H80.
Programs that shuffle, spawn or wander "randomly" therefore produce the same playthrough every time — which is what makes scripted play-testing reproducible.
7. PRINT number formatting
Numbers print at the expression's static type: SINGLE formatting
(~7 significant digits, float32-narrowed) by default, 16 significant
digits for DOUBLE expressions — with D±xx scientific notation
outside the fixed range. Non-negative values get a leading space;
every number gets a trailing space.
Known gap: some classic implementations of this dialect misprint a
few DOUBLE values by 1–2 ulp in a bit-pattern-dependent way
(STR$(592#) prints "591.9999999999999" there, while 321#
prints exactly). Every constant-error hypothesis failed on
same-decade value pairs, so this interpreter prints the
mathematically correct 16 digits instead.
8. Statement budget and introspection
setMaxStatements(n)caps execution; theStepsLimitExceptionfires when the budget is spent. This is how--steps=Nsamples endless game loops deterministically.statementsExecuted()reports how far a run got.variable(name)reads a variable from the global scope — agents assert on program state directly.video()exposes the whole virtual VGA: raw page bytes, PNG dumps, cursor positions, palette entries.- Setting the system property
crtbasic.linelogto a file path makes everyLINEcall appendx1,y1,x2,y2,colorafter coordinate conversion — a debug aid for tracing draw calls.
9. File I/O model
OPEN path FOR INPUT AS #n opens a text file (read as CP437; relative
paths resolve against the .bas file's directory). Each channel keeps
a one-line lookahead so EOF(n) can answer without consuming input,
plus a buffer of comma-split items so several INPUT # statements can
share one line and one INPUT # can span lines. Numeric targets parse
like VAL (empty or unparseable is 0); string targets keep raw text
with surrounding quotes stripped. LINE INPUT # reads whole lines;
CLOSE with no channel closes everything; closing an unopened channel
is silently ignored.
Output modes (OPEN FOR OUTPUT, PRINT #) are not yet implemented.
10. Input consumption
The interpreter maps queued keys to classic byte sequences at
consumption time: printable keys yield their character (Enter as
CHR$(13)); extended keys yield CHR$(0) + scancode. INKEY$
returns a whole sequence in one call; INPUT$(n) doles bytes out
across reads, holding the tail of a half-consumed extended key.
Interactive mode blocks waiting for keys in INPUT$; headless mode
never blocks.