P8X BASIC¶
A small BASIC interpreter for the P8X, written in P8X assembly, assembled by
p8xasm.py and run over the 6850 ACIA serial console —
the same toolchain and I/O the ROM monitor uses.
Using the language? See the P8X BASIC Programmer's Guide — statements, expressions, functions, memory access, and example programs. This README covers build internals and milestones.
Status: runs programs. Editor + tokenizer + integer expression evaluator, and stored programs now execute:
RUN,GOTO,IF…THEN <stmt|line>,END, plus comparisons (= <> < > <= >=),LET. A real branching program works, e.g.:10 LET I=5 20 PRINT I 30 LET I=I-1 40 IF I>0 THEN 20 50 END RUN -> 5 4 3 2 1Plus
FOR/NEXT(withSTEP),GOSUB/RETURN. A real program runs:10 LET S=0 20 FOR I=1 TO 5 30 LET S=S+I 40 NEXT I 50 PRINT S -> 15Plus
INPUT, multi-statement lines (A=1 : PRINT A), multi-itemPRINT A, B; C), single-line loops, signed 16-bit integers with unary minus,REM, and functionsABS,RND,PEEK,POKE(memory + I/O, soPOKE 65282,ndrives the LED port). The full MS-style subset is in.Plus string variables (
A$, 16 slots × 32 chars): assignment,+concatenation,=/<>/</>/<=/>=comparison,INPUT, andLEN/ASC/CHR$/LEFT$/RIGHT$/MID$.Plus
SAVE "NAME"/LOAD "NAME"— programs persist to the CompactFlash filesystem via the monitor's BIOS FS calls, relative to the current directory when running under P8X/OS (a leading/is absolute) — and data files:OPEN name$ [FOR] OUTPUT|INPUT,PRINT#,INPUT#,CLOSE(one sequential channel, one value per record).Plus graphics, one window-space coordinate system (y UP, the PGC's, 480x272 RGB565 direct colour):
COLOR r,g,b(or one packed value),CLS,PIXELW x,y,LINE x0,y0,x1,y1,BOX x0,y0,x1,y1[,FILL|,NOFILL],CIRCLE x,y,r[,ry][,FILL|,NOFILL](a second radius makes it an ellipse), PGC stroke TEXT out of the box (MOVE3 x,y,0 : TEXT s$-- the OS boot-loads /FONT.GL), the easy 2DGTEXT x,y,size,s$,IMAGE x,y,name$(P8I file, bottom-left at x,y), the functionsPIXELR(x,y)(read a pixel back) andRGB(r,g,b)(pack a colour) -- and the whole PGC graphics language as native statements (man basic, GRAPHICS). Two scanout layers compose (drawing bitmap below, the alphanumeric text overlay on top):GRAPHICSON/GRAPHICSOFFandTEXTON/TEXTOFFflip each independently. BASIC cold-starts with GRAPHICS OFF (a fresh interpreter shows only its text); visibility is scanout-only, so drawing while hidden still lands in bitmap RAM andGRAPHICSONreveals it. No display modes, noSCREEN, no palette -- one geometry, and a pixel is its colour. The drawing is done by the DEVICE, so a filled box costs the same few instructions as an empty one. With no display fitted they print?No displayinstead of quietly doing nothing.Lines are syntax-checked on entry (balanced parens, terminated strings, legal statement leader), so typos are caught as you type, not at RUN.
Limits: FOR nesting 3 deep, GOSUB 3 deep; one data file open at a time.
Rewritten for the Tier A ISA (2026-09-12).
p8xbasic.asmwas redone from scratch as a drop-in (same tokens — the.BASformat — same keyword table, messages, HELP and GL byte streams; everymake test-basictest passes unchanged): statements and functions dispatch through tables indexed by the token (STMTTAB/FACTAB, andCHECKLINEreads the same table to decide what may start a statement), the evaluator pushes its running value withPHW/PLWand does its arithmetic with the word ops, a comparison is oneCMPWplus a relation mask shared by numeric and string compares, variables and FOR/GOSUB frames are records addressed with(P1+d), and the line search stops as soon as the sorted program passes the target. 11,151 → 9,124 bytes; cycles (POKE 65282,nstamps,p8xemu -L): a 2,000-iteration arithmetic loop 26.2 M → 16.5 M, a GOSUB/12-variable loop 37.7 M → 18.9 M, a string concat/compare loop 16.3 M → 5.8 M. Behaviour changes, all deliberate: FOR nests 3 deep (was 2, unchecked), a 4th GOSUB or a 33rd variable is a?SYNTAX ERRORinstead of silent corruption, lowercase variable names work at the start of a statement, and a statement afterRUNon the same line is no longer misparsed.
Direction¶
A richer Microsoft-style subset, integer-only (decided 2026-06-16). Line- numbered and interactive, with immediate mode (no line number → execute now). This fits the machine well — the pointer bank makes a text pointer (P1/P2) and the indirect addressing modes natural for the interpreter inner loop; integers keep it tractable on this ISA (floats are a large lift, deferred).
Target language:
- Statements: PRINT, LET (and implicit let), IF/THEN, FOR/NEXT,
GOTO, GOSUB/RETURN, INPUT, REM, END, RUN, LIST, NEW,
SAVE/LOAD, and data files (OPEN/CLOSE/PRINT#/INPUT#)
- Lines: multiple statements per line separated by :
- Expressions: integer + - * /, parens, comparisons (= <> < > <= >=),
named numeric variables (≤6 significant chars, up to 32), and string
variables (A$: assign, + concat, compare) with + concatenation
- Functions: ABS, RND, PEEK/POKE (memory + I/O access — the P8X
hook), and the string functions LEN, ASC, CHR$, LEFT$, RIGHT$, MID$
Build & run¶
Interactive — type BASIC at a live prompt:
./basic/run.sh
This assembles the interpreter, builds the microcode, compiles the emulator, and launches it attached to your terminal. The emulator detects the TTY and runs the console in raw/blocking mode (no cycle cap, no busy-spin), so you can type lines directly. Quit with Ctrl-C, or Ctrl-D at the prompt.
Scripted — pipe a session (for tests/demos):
python3 assembler/p8xasm.py basic/p8xbasic.asm -o /tmp/basic.bin
(cd microcode && python3 genucode.py) # build u0-u3.bin
cp microcode/u?.bin /tmp/
printf '20 PRINT "B"\r10 PRINT "A"\rLIST\r' | (cd /tmp && \
"$OLDPWD/emulator/p8xemu" -l 8000000 basic.bin)
Lines are terminated by CR (\r). In scripted mode use a cycle cap -l N to
bound the spin after end-of-input.
Three ways to build & run (one source)¶
BASIC is self-contained (its own ACIA console + RAM), so the same source
builds several ways. The differences are just -D symbols — BASORG (code
origin), BASRAM (data base), PBUF (rebuild scratch), and MONITOR (where
BYE goes). All default to the standalone values and are overridable per build.
| Build | Code (BASORG) |
Data (BASRAM) |
Invoked by |
|---|---|---|---|
| ~~Standalone~~ | $0000 |
$8000 |
RETIRED (2026-08-13) — see the note below |
| Disk | $2000 |
$A000 |
installed on a P8XFS image, booted by the monitor B command (rev E: loads at $2000) |
| Run-from-OS | $5900 |
$C500 |
a TPA program (PBUF=$E000, MONITOR=$2000) installed as BASIC.BIN; RUN it from the OS, BYE returns to the OS (see below) |
The standalone build no longer works. BASIC's
PUTC/GETCnow tail-call the BIOS (CONOUT$0103/CONIN$0100) instead of driving the ACIA directly, so it needs the monitor resident at$0000-$1FFF— which the$0000build replaces. Nothing had built that variant for some time (every target passes-D BASORG=$2000or$5900, andbuild_rom.shstates BASIC is no longer ROM-resident), so this formalises an existing state rather than removing a capability. The source defaults are still the$0000values; restoring the target would mean giving BASIC back its own console routines.
Code is where the interpreter runs (low ROM or RAM); Data is
the base of its variables + program text; PBUF (rebuild scratch) defaults to
$C000 and moves only for the TPA build. The standalone build takes no -D (the
source defaults are $0000/$8000/$C000) and is byte-identical to before this
split.
ROM-in-monitor is gone. BASIC used to be overlaid into the monitor EEPROM at
$2000and launched with the monitorXcommand. Since it also ships as a disk program, the ROM copy was redundant and was removed to reclaim ROM space; use the Disk or Run-from-OS build below.
Disk — assemble at $2000 (rev E boot address), install as a bootable image, boot with B:
python3 assembler/p8xasm.py basic/p8xbasic.asm -o basicdisk.bin \
--base 0x2000 -D BASORG=0x2000 -D BASRAM=0xA000
python3 tools/p8xfs.py create disk.img
python3 tools/p8xfs.py boot disk.img basicdisk.bin
./emulator/p8xemu -c disk.img eeprom.bin # at '*' press B
Run from P8X/OS — the primary way to use BASIC: install it as a regular OS
program so you can RUN BASIC.BIN from the OS shell and BYE back to it. No source change —
just relocate everything into the TPA ($5900+, clear of the OS at
$2000–$AFFF) and point MONITOR at the OS cold-start so BYE re-enters the OS
(which stays resident) instead of the ROM monitor:
python3 assembler/p8xasm.py basic/p8xbasic.asm -o basicrun.bin \
--base 0x5900 -D BASORG=0x5900 -D BASRAM=0xC500 -D PBUF=0xE000 -D MONITOR=0x2000
python3 tools/p8xfs.py put disk.img basicrun.bin --name BASIC.BIN --load 0x5900 --exec 0x5900
# boot the OS (B), then: RUN BASIC.BIN ... use BASIC ... BYE (-> back at /> )
Layout: code $5900–$8DAx (~8.9 KB), data $C500 (PROG at $CA80), rebuild
buffer $E000; the stack stays at $FEFF. Covered by os_basic_test.sh.
These paths are covered by make test-basic (in emulator/): disk BASIC via
B, SAVE/LOAD round-trip, string variables/functions, data-file I/O, and
RUN BASIC.BIN from the OS. Code is ~8.9 KB, so in every layout it clears its
data base with room to spare.
The C version (basic.c) — size and speed against the asm one¶
basic/basic.c is the same interpreter written in C (2026-09-13): same
language, tokens (the .BAS format), messages, prompts, limits and GL byte
streams, and it keeps its big tables in the same raw memory the asm build uses
($C600 variables … $CA80 program, $E000 rebuild scratch), so both binaries
are code plus a little data. It is built by the host toolchain — the generated
GL verb tables (glkwtab.c, from generators/gen_glkw.py) concatenated ahead
of the source, //#use abi spliced by clib.py, compiled by p8cc.py — and
installed as /binc/basic.bin; the asm build stays the /bin default.
emulator/test/basic_c_test.sh (in make test-basic) runs the same programs
the asm tests use against it, and a 250-line scripted session diffs identically
between the two.
asm p8xbasic.asm |
C basic.c |
ratio | |
|---|---|---|---|
| binary | 9,124 B | 21,393 B | 2.3× |
2,000-iteration arithmetic loop (S=S+I*3-I/2) |
16.5 M cycles | 59.6 M | 3.6× |
| 400 iterations of GOSUB + 12 variables + IF | 18.9 M | 76.1 M | 4.0× |
| 300 iterations of string concat / LEFT$ / MID$ / compare | 5.8 M | 28.2 M | 4.9× |
(Cycle stamps: POKE 65282,n at the start and end of the program, read with
p8xemu -L.) The C source is written the way this compiler wants it — it has no
break/continue, int compares and divides unsigned (a signed BASIC compare
flips the sign bit of both sides; a while (n >= 0) on an int never ends),
there is no longjmp (an error sets a flag every parse level returns through),
and a multiply by a constant is a 16-step __mul loop, so the hot paths use
int-pointer loads, shifts and adds instead (vget/vset, recnum,
parsedec), and the name compare and blank skipping are inlined in the
expression path. That tuning took it from 8–13× slower to the 3.6–4.9× above;
what remains is the call overhead of the recursive-descent parser (a frame per
call) and the byte-at-a-time pointer walks the compiler emits. Note for anyone
running the two side by side: the C image reaches $BD00, so a BASIC program
that POKEs into $A000–$BC00 (free under the 9 KB asm build) corrupts it.
Planned layout (proposed — see open decisions)¶
| Region | Use |
|---|---|
$0000-$1FFF |
interpreter code (EEPROM, 6 KB) |
$2000-$7FFF |
RAM (rev E) — unused by the standalone build |
$8000-… |
tokenized program text (standalone BASRAM=$8000) |
…-$FDFF |
named numeric vars (32) + string-var table + string/eval scratch |
$FE00-$FEFF |
stack (P3), incl. GOSUB return stack |
Milestones¶
- REPL skeleton — banner + line input/echo. ✅
- Line editor — store numbered lines sorted (insert/replace/delete by line
number),
LIST,NEW. ✅ (rebuild-via-scratch-buffer; 16-bit decimal I/O) - Expression evaluator — integer
+ - * /, parens, variables (A–Z). ✅ recursive-descent; 16-bit mul/div helpers. Wired to immediatePRINT/LET. - Statements + RUN — execute the stored program.
✅
RUN,GOTO,IF/THEN,END, comparisons,FOR/NEXT(+STEP),GOSUB/RETURN,INPUT, multi-statement lines (:), multi-itemPRINT. - Polish — ✅ signed integers + unary minus,
REM, functionsABS,RND(LCG),PEEK/POKE(memory + memory-mapped I/O), andSAVE/LOADover the filesystem. - Strings, files, entry-time checking — ✅ string variables (
A$) with assignment/concat/comparison andLEN/ASC/CHR$/LEFT$/RIGHT$/MID$; sequential data files (OPEN/CLOSE/PRINT#/INPUT#); and aCHECKLINEstructural syntax check applied as each line is entered.
Open decisions¶
Settled: dialect = richer MS-style subset; numbers = integer-only;
storage = keywords tokenized to single bytes (≥$80), strings/text left
literal — crunch on entry, uncrunch in LIST. Still open:
- Relationship to the system — standalone ROM image (current, easiest to test),
vs launched from the monitor's
G, vs loaded from CF by the OS.
Leaving BASIC (BYE)¶
Under P8X/OS (the run-from-OS build), BYE returns to the shell that ran it:
the current directory, any redirection, and the rest of the OS state survive.
It used to JMP MONITOR, which for that build is $2000 — the OS's COLD
entry. Leaving BASIC therefore rebooted the OS: it reprinted the banner and put
you back in the root however deep you had cd'd. Since 2026-08-13 BASIC captures
the caller's stack pointer at entry (SPSAV) and BYE restores it and RTSes,
which is exactly how every /bin program returns — the OS launches them with
JSR (P1).
Two consequences inside BASIC, both deliberate:
- Under the OS, BASIC adopts the caller's stack rather than resetting
P3toSTKTOP; resetting it would overwrite the caller's frame, including the return address it needs. SYNERRunwinds to that saved pointer instead ofSTKTOP, for the same reason.
The disk-boot build has no caller, so it still owns the whole stack and BYE
jumps to the reset vector as before.