Skip to content

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