basic¶
the P8X BASIC interpreter
NAME
basic - the P8X BASIC interpreter
SYNOPSIS
basic
DESCRIPTION
P8X BASIC is a small integer BASIC with string variables, functions,
and data-file I/O. Run it with `basic` (or `run /bin/basic.bin`); type
BYE to return to the OS. SAVE/LOAD and data files use the shared
filesystem, so programs and data are visible to dir and the other
tools. This page is a quick reference; the full guide lives in the
repo (basic/p8x-basic-guide.md).
MODES
Immediate a line with no leading number runs at once:
PRINT 2+3*4 -> 14
Program a line that starts with a number is stored:
10 PRINT "HELLO"
Type RUN to execute the stored program. Entering "30 ..."
inserts or replaces line 30; a bare "30" deletes it. Lines
are kept sorted by number. Every line is syntax-checked as
you type it (balanced parens, closed strings, a legal
statement), so typos are caught at entry, not at RUN.
NUMBERS AND VARIABLES
Numbers are signed 16-bit integers, -32768..32767, in decimal or hex
with a 0x prefix (0x1F). Arithmetic wraps modulo 65536.
Numeric variables start with a letter, continue with letters/digits,
are case-insensitive and significant to 6 characters, up to 32 of
them; each starts at 0. No arrays.
String variables end in `$` (A$, NAME$), up to 16, each holding up to
32 characters (longer values truncate); they are a separate namespace
from numeric variables and start empty. Join strings with `+`.
OPERATORS
Highest precedence first:
unary - +
1 * / % (integer; / truncates toward zero)
2 + -
3 = <> < > <= >= comparisons yield 1 (true) or 0 (false)
Parentheses override precedence. Comparisons are signed and usable
anywhere a number is. Strings compare too (lexicographic), so
IF A$ = "Y" and IF N$ < "M" work.
FUNCTIONS
ABS(x) absolute value
RND(n) pseudo-random integer 1..n
PEEK(addr) byte (0-255) at a memory address
PIXELR(x,y) colour at a pixel (window coords); 0 if off-screen
RGB(r,g,b) pack a colour: r,b 0-31, g 0-63 (see GRAPHICS)
LEN(s$) number of characters in s$
ASC(s$) code of the first character (0 if empty)
CHR$(n) one-character string with code n
LEFT$(s$,n) first n characters
RIGHT$(s$,n) last n characters
MID$(s$,i[,n]) n chars from position i (1-based); to end if n omitted
STR$(x) the decimal text of x (STR$(-7) is "-7")
VAL(s$) number parsed from s$ (signed; stops at first non-digit)
EOF(n) 1 if the input file is at end (or not open), else 0
Counts clamp to the source, so LEFT$("HI",9) is just "HI".
STATEMENTS
Several may share a line, separated by `:`.
PRINT items print numbers/strings; `;` = no gap, `,` = one space;
a trailing `;`/`,` suppresses the newline
LET v = expr assign (LET optional); works for A$ too
IF expr THEN .. run the rest of the line if expr is non-zero; the THEN
part is a statement or a line number (implicit GOTO)
FOR v = a TO b [STEP s] / NEXT [v] counting loop
GOTO line / GOSUB line / RETURN
INPUT v prompt "? " and read a number, or a whole line into A$
POKE addr,val store the low byte of val at addr
REM text comment
END stop the program
GRAPHICS
Drawing goes to the display device, if one is fitted; without it these
print ?No display rather than doing nothing. Under the emulator there
is no live window: press Ctrl-\ to draw the screen in your terminal
and carry on, or Ctrl-C to draw it and quit. The screen is 480x272 in
RGB565 direct colour: a pixel IS its colour, 65,536 of them, and 0 is
black. ONE coordinate system: x runs 0..479 left to right, y runs
0..271 BOTTOM to top -- window space, the PGC's own, everywhere.
Off-window drawing clips; off-screen device pixels are simply not
drawn.
COLOR r,g,b drawing colour: r,b 0-31, g 0-63...
COLOR c ...or ONE packed RGB565 value, for
RGB() and PIXELR() round-trips
DRAWING statements (MIGRATED 2026-08-30: these emit PGC/GL now --
WINDOW space, y UP, transformed by WINDOW/VWPORT, honouring
LINPAT/LINFUN, recording inside CLBEG/CLEND. BASIC establishes the
full-screen window at startup and again after a RESETF statement,
so out of the box window (x,y) is screen (x, 271-y)):
CLS clear (GL FLOOD 0,0,0 over the
current viewport); COLOR unchanged
PIXELW x,y one pixel (GL MOVE + POINT)
LINE x0,y0,x1,y1 line, both endpoints drawn
BOX x0,y0,x1,y1 rectangle outline (GL RECT)
BOX x0,y0,x1,y1,FILL ... solid
BOX x0,y0,x1,y1,NOFILL ... outline, spelled out
CIRCLE x,y,r circle outline (GL ELIPSE rx=ry;
radius maps through the window scale,
caps at 255 device px, r=0 draws
nothing, negative radius = GL err 2)
CIRCLE x,y,r,FILL ... solid
CIRCLE x,y,rx,ry ELLIPSE: separate x and y radii
CIRCLE x,y,rx,ry,FILL ... solid
GRAPHICSON / GRAPHICSOFF show / hide the drawing bitmap
(default OFF in BASIC)
TEXTON / TEXTOFF show / hide the alphanumeric text
overlay (the console char plane)
BOX and CIRCLE force their own fill mode and then restore the
program's PRMFIL from a shadow the native PRMFIL/RESETF statements
keep -- a GL "PF 1" STRING bypasses that shadow.
IMAGE x,y,name$ draws a P8I image file with its BOTTOM-LEFT
corner at (x,y) -- SINGLE-INTERFACE since 2026-09-01: it is the
GL BLIT verb now (one per row, the file bytes streamed verbatim),
so the anchor maps through the CURRENT WINDOW/VWPORT like every
other statement -- the old fixed-mapping caveat is GONE, and with
it BASIC's last device-door write. The pixels themselves stay
1:1 device pixels (no scaling). On the card this replaces ~14
wire bytes and a round-trip poll per pixel with 2 streamed bytes.
LAYERS. The panel composites two planes at scanout: the drawing
bitmap (everything the statements above draw) and an alphanumeric
TEXT overlay (the console character plane) on top of it. Each has
its own visibility switch, and both may be on at once:
GRAPHICSON / GRAPHICSOFF the drawing bitmap
TEXTON / TEXTOFF the text overlay
BASIC cold-starts with GRAPHICS OFF, so a fresh interpreter shows
only its text; a program that draws issues GRAPHICSON to reveal the
bitmap. Visibility is SCANOUT-ONLY: GRAPHICSOFF hides the bitmap
without erasing it, and drawing while hidden still lands in bitmap
RAM (PIXELR reads it back) -- so you can compose a whole scene
off-screen and reveal it in one GRAPHICSON. BYE restores GRAPHICSON
on the way out, so the shell and other apps get the bitmap back.
TEXT is the PGC's own stroke text, drawn card-side from the font
the OS streams from /FONT.GL at boot (the glyph bank survives
RESETF -- a font is INSTALLED, not drawn). The EASY form:
GTEXT x,y,size,s$ 2D text sugar (reborn 2026-09-01 as
pure GL emission -- the PGC port,
never the device): window coords,
baseline-left, ABSOLUTE size (the
old multiplier: 1 = 1x, 2 = 2x),
correct in ANY session state. The
deliberate cost: it resets the
modeling matrix and camera each
call -- 3D work uses the raw verbs.
The raw idiom:
MOVE3 x,y,0 : TEXT s$ baseline-left at (x,y); TSIZE n
scales (256 = 1x) and TANGLE
rotates -- both COMPOSE into the
modeling matrix like every matrix
verb (TSIZE 512 twice is 4x, and
the ANCHOR scales too: model
coords). MDIDEN resets the matrix;
TSIZE 256 is a no-op, NOT a
restore. TJUST justifies
BASIC cold-starts with PROJCT 0 (text strokes live at z=0, which
the native camera near-clips); RESETF restores the native camera
for 3D work -- issue PROJCT 0 again for text after it. A saved
program's GTEXT line reports ?SYNTAX ERROR, like PALETTE's.
PIXELR(x,y) FUNCTION: read a pixel's colour (was
POINT()). SINGLE-INTERFACE since
2026-08-31: it is the GL verb PIXRD
now, so it maps through the CURRENT
WINDOW/VWPORT exactly like PIXELW --
PIXELW x,y then PIXELR(x,y)
round-trips in ANY window
Everything in the GL section further down (MOVE/DRAW/POLY/RECT,
the 3D and matrix verbs, CIRCLE-as-GL, AREA, TEXT, patterns...)
is the PGC language: window space, y UP, through the $FF50 port.
Any two opposite corners work for BOX; they are sorted for you. The
drawing itself is done by the device, not by BASIC, so a filled box is
as quick as an empty one.
There is no SCREEN statement, no display modes and no palette. The
device is 480x272 at 16 bits a pixel and nothing else. A colour is
RGB565 -- five bits of red, six of green (the eye is fussiest there),
five of blue:
COLOR 31,0,0 red (r and b are 0-31, g is 0-63)
COLOR 31,63,31 white
The one-number form takes a PACKED colour, which is what RGB(r,g,b)
builds and what PIXELR returns -- so C=PIXELR(X,Y):COLOR C draws with a
colour read off the screen. Arguments are masked to their fields.
Because BASIC's integers are signed 16-bit, bright colours PRINT as
negative numbers -- red is -2048 -- but compare and store perfectly
well.
IMAGE draws a picture file. The file carries its own geometry and
depth (the P8I format -- tools/p8img.py converts anything into it), so
the statement takes only the position; a file that is not P8I, is the
wrong version or depth, or ends early says ?NOT P8I, and a missing one
?No file. Pixels past the window edge clip, so an image can hang
off any side. IMAGE uses the data channel, so a file OPEN'd for
INPUT is closed by it -- the same licence SAVE and LOAD take.
GTEXT is 2D sugar over the card's own stroke text (see the GTEXT
entry above): a few GL bytes per call, not per pixel -- fast enough
for anything, but still not a console: there is no cursor, no
scrolling and no line wrap.
GTEXT 10,10,1,"SCORE" baseline at (10,10), text rises
GTEXT 0,0,2,A$+"!" any string expression works
size multiplies the glyph (1 = 1x, 2 = 2x). Text is drawn in the
current COLOR. Only codes $20-$5F have glyphs -- lowercase folds
onto uppercase, and anything else comes out blank. Strokes clip at
the window edge like every GL primitive; there is no wrap.
PIXELR(x,y) is a FUNCTION, not a statement: it returns the COLOUR at a
pixel, and 0 for anything off-screen. So
IF PIXELR(10,10) = 0 THEN PIXELW 10,10
works, and comparing against the same RGB() you drew with matches
exactly:
IF PIXELR(10,10) = RGB(31,0,0) THEN PRINT "STILL RED"
(A bright colour prints as a negative number -- see above -- but the
comparison is bit-for-bit either way.)
THE GRAPHICS LANGUAGE (3D)
With a GL engine fitted (see man gl), the graphics language's verbs
are BASIC statements in their own right: windows and viewports, 3D
moves and draws, filled polygons, the modeling and viewing matrix
families, perspective, command lists and page flips. Arguments are
ordinary expressions, comma-separated:
WINDOW -120,120,-120,120 2D window (x1,x2,y1,y2!)
VWPORT 104,375,0,271 screen viewport
PRMFIL 1 closed primitives fill
MDROTY A*2 compose a rotation, in degrees
DRAW3 90,-90,300 3D line from the 3D current point
POLY3 3,-80,-80,300,80,-80,300,0,40,420
CLBEG 1 ... CLEND record statements into list 1
CLOOP 1,7 replay it 7 times (deltas add up)
FLIP show the drawn page
The full set: MOVE MOVER DRAW DRAWR RECT RECTR POLY POLYR PRMFIL
WINDOW VWPORT FLOOD CLEARS / MOVE3 MOVER3 DRAW3 DRAWR3 POINT3 POLY3
POLYR3 CONVRT / MDIDEN MDORG MDROTX MDROTY MDROTZ MDSCAL MDTRAN
MDMATX / VWIDEN VWRPT VWROTX VWROTY VWROTZ VWMATX DISTAN PROJCT
DISTH DISTY CLIPH CLIPY / CLBEG CLEND CLRUN CLOOP CLDEL CLAPP /
FLIP PGSYNC WAIT RESETF LINFUN AREA AREABC TEXT TSIZE TANGLE
TDEFIN ELIPSE LINPAT TEXTP TJUST. Verbs,
parameters and order are exactly
the device's (man gl documents each); FLOOD and CLEARS take r,g,b
the way the device does. COLOR is not in the list because the
ordinary COLOR statement now sets the GL pen too -- one pen
statement drives both drawing paths. POINT (the GL verb: draw the
2D current point) IS native now -- the pixel-read function became
PIXELR() and freed the name. GL CIRCLE is not either: BASIC's own
CIRCLE x,y,r statement (window space since the 2026-08-30
migration, with its centre argument pair) keeps the name, and
GL "CIRCLE r" (at the GL current point) reaches the verb. Between
CLBEG and CLEND these statements RECORD instead of drawing, so a
program can build a command list -- the migrated drawing
statements record too. Without a GL engine they say ?No GL engine.
LINFUN m (stage 10f) sets the pixel-write mode for lines, points
and outlines from EVERY drawing statement -- BASIC's own LINE and
PIXELW included, since the mode lives in the display device:
0 replace, 1 complement (invert what is there; pen ignored), 2 OR,
3 AND, 4 XOR. Draw a line twice in XOR and it is GONE, the picture
under it intact -- the rubber-band idiom. Fills always replace.
RESETF (or a reboot) returns to replace mode.
AREA (stage 10g) seed-fills from the GL 2D current point with the
pen, bounded by pen-coloured pixels: outline a shape, MOVE inside,
AREA. AREABC r,g,b bounds on a stated colour instead, so outline
and fill can differ. A seed outside the window is a GL error; a
seed on the boundary quietly fills nothing. AREA always paints in
replace mode, whatever LINFUN says.
TEXT s$ (stage 10h) draws vector text at the current 3D point --
any string expression. Load the font once per power-up (the OS
ships it): from the shell `gl /FONT.GL`, or in a program stream it
with GL. Use the ortho camera for window-space text, place with
MOVE3, and size/rotate with TSIZE (8.8, 256 = design size) and
TANGLE (degrees) -- both compose like matrix verbs, about MDORG:
10 RESETF : PROJCT 0 : CLEARS 0,0,0
20 WINDOW 0,479,0,271 : VWPORT 0,479,0,271
30 COLOR RGB(31,63,0) : TSIZE 512
40 MOVE3 40,100,0 : TEXT "HELLO"
TDEFIN c records a custom glyph for char c through the same native
statements (strokes as MOVER3/DRAWR3, CLEND finishes), so a program
can define its own symbols. man gl has the full text model.
ELIPSE rx,ry draws a window-space curve at the GL current point
(PRMFIL 1 fills; rx=ry is the circle -- the CIRCLE name stays
with the DEVICE statement. ARC and SECTOR were removed
2026-08-30: draw arcs as short DRAW chains).
LINPAT p sets the device line pattern -- BASIC's
own LINE and PIXELW honour it, like LINFUN; -1 restores solid.
TJUST h,v
justifies TEXT (1..3 each: left/centre/right, bottom/middle/top);
TEXTP s$ is TEXT s$ under its PGC name. man gl has the details.
Start a scene with RESETF: the machine draws a boot splash through
the same engine, and matrix verbs COMPOSE -- without a reset your
first rotation lands on top of whatever came before.
Native statements are SYNCHRONOUS -- each waits for the card to
finish, so PIXELR() right after a draw reads finished pixels. A
native CLOOP therefore blocks until the replay ends; launch a long
fly-through with GL "CLOOP 0 100" instead: the text path does not
wait, and the card animates while BASIC runs on.
GL s$ sends one raw graphics-language line as TEXT, for anything
else (short forms, the GL POINT, verbs newer than this page):
GL "MDY "+STR$(A)
Mind the 32-character string limit: a GL line longer than that
truncates, and a POLY3 with many coordinates will not fit -- that
is what the native statements are for.
DATA FILES
One sequential channel over the filesystem:
OPEN name$ [FOR] OUTPUT|INPUT open (name may be a path)
PRINT# expr write one value + newline
INPUT# var read one record (number or A$)
CLOSE commit/close
EOF(n) 1 at end of the input file, else 0
One value per record; EOF(n) ends a read loop (IF EOF(1) THEN ...).
Opening a missing file for INPUT prints ?No file.
COMMANDS
RUN execute the stored program
LIST print the program
NEW erase the program (and variables)
SAVE "path" write the program to a file
LOAD "path" replace the program with a saved file
HELP summary of statements/commands/functions
BYE leave BASIC (returns to the OS)
SAVE/LOAD and OPEN take a PATH: a bare name is relative to the OS
current directory, and a leading slash is absolute (SAVE "/SRC/GAME").
Leaf names are up to 12 characters and case-sensitive. Make
subdirectories with MKDIR in the OS first.
MEMORY
PEEK/POKE reach the whole map (decimal addresses): 0-8191 EEPROM
(read-only), 8192-65279 RAM, 65280 switch input, 65282 LED output,
65284/65285 the serial port, 65360-65364 the display's GL port,
65328-65340 the MDU (a hardware multiply-divide unit: poke operands
a,b,c as 16-bit pairs at 65328/65337, 65329/65338, 65330/65339,
poke 65332 to start, then peek 65331/65340 for (a*b)/c -- signed,
saturating at +/-32767). So POKE 65282,170 lights an LED pattern.
The graphics statements cover the whole GL language, so poking
65360 (one GL byte per POKE, after checking bit 7 of PEEK(65361))
is only for exploring the port by hand -- the GL statement does the
same with less typing.
ERRORS
? unrecognized statement/command
?No display a graphics statement with no display fitted
?SYNTAX ERROR malformed line (also reported at entry)
?SYNTAX ERROR IN n the same, during RUN — names the failing line n
?UNDEF'D LINE GOTO/GOSUB to a missing line
?RETURN WITHOUT GOSUB RETURN with no matching GOSUB
A running program stops and returns to the prompt on any error.
LIMITS
Integers only (no floating point). FOR loops nest 3 deep, GOSUB 3
deep. Data files: one channel, one value per record (EOF(n) tests for
end). No DATA/READ, DIM, DEF FN, ON..GOTO, or WHILE.
EXAMPLES
basic
10 FOR I=1 TO 5 : PRINT I*I : NEXT
RUN
SAVE "/SRC/SQUARES"
BYE
SEE ALSO
edit, run, save, load, mkdir
See also: edit, run, save, load, mkdir
The text of man basic on P8X (os/man/basic in the repository). All commands