_____ ____ __ __ ____ _____ _____ _____
/ ____| _ \| \/ | | _ \ /\ / ____|_ _/ ____|
| | | |_) | \ / |______| |_) | / \ | (___ | || |
| | | _ <| |\/| |______| _ < / /\ \ \___ \ | || |
| |____| |_) | | | | | |_) / ____ \ ____) |_| || |____
\_____|____/|_| |_| |____/_/ \_\_____/|_____\_____|
............................................................
CBM-BASIC is, as the name suggests, a BASIC interpreter inspired by CBM BASIC v2 as found on classic Commodore machines. This means while you can use GOTO, you don't have to. You do you. This is a readme file, not the code police.
Unlike emulators, this is a BASIC interpreter that, while a project still very much in development, can already do real work. For example, this interpreter is capable of reading and writing files on your computer's disk.
The project started as an expansion on an original LLM-generated demo project by David Plummer. As I played with it, and added to it, I had fun. That's enough reason for me, and nobody is going to take your favourite toolchain away because this exists so calm down, sheesh.
See CHANGELOG.md for a versioned history of changes (starting from 1.0.0 – 2026-03-09).
The latest binaries for Win/Mac/Linux are in Releases.
Extract the files after downloading.
Each release archive includes the interpreter binary and the examples folder so you can run programs such as ./basic examples/trek.bas (or basic.exe examples\trek.bas on Windows) from the unpacked directory.
On recent macOS versions, downloading a binary from the internet may show a warning such as:
“Apple could not verify
basicis free of malware that may harm your Mac or compromise your privacy.”
Because basic is an unsigned open‑source binary, this is expected. To run it anyway:
- First attempt to open it once (so Gatekeeper records it):
- In Finder, locate the unpacked
basicbinary. - Control‑click (or right‑click) and choose Open.
- When the warning dialog appears, click Cancel.
- Allow the app in System Settings:
- Open System Settings → Privacy & Security.
- Scroll down to the Security section; you should see a message about
basicbeing blocked. - Click Open Anyway, then confirm in the dialog.
- After this, you should be able to run
./basicfrom Terminal without further prompts.
If you prefer using Terminal only, you can also remove the quarantine attribute:
xattr -d com.apple.quarantine /path/to/basicRun this once after unpacking, and macOS will stop treating the binary as “from the internet”.
- Meta directives (load-time,
#prefix) — seedocs/meta-directives-plan.md:- Shebang:
#!/usr/bin/env basicon first line (ignored). #OPTION petscii/#OPTION charset lower/#OPTION palette c64— mirror command-line options; file overrides CLI.#INCLUDE "path"— splice another file at that point. Recommend numberless mode when using includes; duplicate line numbers and labels error.
- Shebang:
- Programs with or without line numbers
- Classic line-numbered programs loaded from a text file (
10 ...,20 ..., etc.). - Also supports numberless programs: if the first non‑blank line has no leading digits, synthetic line numbers are assigned internally (you can still use labels and structured control flow).
- Classic line-numbered programs loaded from a text file (
- Core statements
PRINT/?: output expressions, with;(no newline) and,(zone/tab) separators; wrapping defaults to a 40‑column C64‑style width.INPUT: read numeric or string variables from standard input, with optional prompt.GET: non-blocking single‑key input into a string variable (e.g.GET K$).- If a key is waiting,
K$becomes a 1‑character string. - If no key is waiting,
K$becomes"". - Enter/Return is returned as
CHR$(13)soASC(K$)=13works for “press Enter” checks.
- If a key is waiting,
LET(optional): assignment; you can also assign withA=1withoutLET.IF ... THEN/IF ... ELSE ... END IF: conditional execution. InlineIF X THEN 100orIF X THEN PRINT "Y"; multi-line blocks withIF X THEN…ELSE…END IF. Supports nested blocks.WHILE…WEND: pre-test loops.WHILE X < 5…WEND; supports nested WHILE/WEND.GOTO: jump to a given line number or label (e.g.GOTO 100orGOTO GAMELOOP).GOSUB/RETURN: subroutines with a fixed-depth stack; targets may be line numbers or labels.ON expr GOTO/ON expr GOSUB: multi-branch jumps; e.g.ON N GOTO 100,200,300orON K GOSUB 500,600.FOR/NEXT: numeric loops, includingSTEPwith positive or negative increments.DIM: declare 1‑D or multi‑dimensional numeric or string arrays (e.g.DIM A(10),DIM B(2,3)).REMand': comments to end of line.END/STOP: terminate program execution.READ/DATA: load numeric and string literals fromDATAstatements into variables.RESTOREresets the DATA pointer so the nextREADuses the first value again (C64-style).DEF FN: define simple user functions, e.g.DEF FNY(X) = SIN(X).POKE: accepted as a no‑op (for compatibility with old listings; it does not touch real memory).CLR: resets all variables to 0/empty, clears GOSUB/FOR stacks and DATA pointer; DEF FN definitions are kept.- File I/O (CBM-style):
OPEN lfn, device, secondary, "filename"— open a file (device 1 = disk/file; secondary 0 = read, 1 = write, 2 = append). Filename is a path in the current directory.PRINT# lfn, expr [,|; expr ...]— write to the open file (likePRINTto file).INPUT# lfn, var [, var ...]— read from the open file into variables (one token per variable; comma/newline separated).GET# lfn, stringvar— read one character from the file into a string variable.CLOSE [lfn [, lfn ...]]— close file(s);CLOSEwith no arguments closes all.ST— system variable set afterINPUT#/GET#: 0 = success, 64 = end of file, 1 = error / file not open. Use e.g.IF ST <> 0 THEN GOTO done.
- Variables
- Numeric variables:
A,B1,SALE,SAFE,PLAYERSCORE, etc. Full variable names up to 31 characters are significant (e.g.SALEandSAFEare distinct). - String variables: names ending in
$, e.g.A$,NAME$. - Reserved words (e.g.
IF,FOR,PRINT) cannot be used as variable names. Labels may match keywords (e.g.CLR:). - Arrays: 1‑D or multi‑dimensional, e.g.
A(10),A$(20),C(2,3). Index is 0‑based internally;DIM A(10)allows indices0..10;DIM C(2,3)gives a 3×4 matrix.
- Numeric variables:
- Intrinsic functions
- Math:
SIN,COS,TAN,ABS,INT,SQR,SGN,EXP,LOG,RND. - Strings:
LEN,VAL,STR$,CHR$,ASC,INSTR.UCASE$,LCASE$: convert string to upper or lower case (ASCII).MID$,LEFT$,RIGHT$with C64‑style semantics:MID$(S$, start)orMID$(S$, start, len)(1‑basedstart).LEFT$(S$, n)– firstncharacters.RIGHT$(S$, n)– lastncharacters.
INSTR(source$, search$)– returns the 1‑based index ofsearch$insource$, or0if not found.
- Formatting:
TABandSPCfor horizontal positioning inPRINT. - Numeric/string conversion:
DEC(s$)– parse a hexadecimal string to a numeric value (e.g.DEC("FF")= 255, invalid strings yield 0).HEX$(n)– format a number as an uppercase hexadecimal string (no$prefix), using the integer part ofn.
- Command-line arguments and shell (for scripting):
ARGC()— returns the number of arguments passed after the script path (e.g../basic script.bas a b→ARGC()= 2). UseARGC()with parentheses.ARG$(n)— returns the nth argument as a string.ARG$(0)is the script path;ARG$(1)…ARG$(ARGC())are the arguments. Out-of-range returns"".SYSTEM(cmd$)** — runs a shell command (e.g.SYSTEM("ls -l")), waits for it to finish, and returns its exit status (0 = success).EXEC$(cmd$)— runs a shell command and returns its standard output as a string (up to 255 characters; trailing newline trimmed). Use for scripting (e.g.U$ = EXEC$("whoami")).
- Math:
SLEEP: pause execution for a number of 60 Hz “ticks” (e.g.,SLEEP 60≈ 1 second).LOCATEandTEXTAT: screen cursor positioning and absolute text placement (see below).CURSOR ON/CURSOR OFF: show or hide the terminal cursor using ANSI escape codes (ESC[?25h/ESC[?25l).COLOR n/COLOUR n: set the text foreground colour using a C64-style palette index0–15, mapped to ANSI SGR colours (approximate CBM palette).BACKGROUND n: set the text background colour using the same C64-style palette index0–15, mapped to ANSI background SGR codes (e.g. 0=black, 1=white, 2=red, 3=cyan, etc.).
- Inline
{TOKENS}:- Within double-quoted strings, the interpreter recognises
{...}patterns and expands them toCHR$()calls at load time. - This lets you write readable code such as:
- Within double-quoted strings, the interpreter recognises
PRINT "HELLO {RED}WORLD{WHITE}"
PRINT "{CLR}READY."
PRINT "MOVE {DOWN}{DOWN}{RIGHT} HERE"- What it does:
- Each
{TOKEN}inside a string is replaced as if you had written";CHR$(N);"at that point in the source, so all existing PETSCII/ANSI mappings forCHR$apply unchanged. - Tokens can be:
- Numeric: e.g.
{147}→CHR$(147). - Named PETSCII codes from the built-in table, including:
- Colors:
WHITE,RED,CYAN,PURPLE,GREEN,BLUE,YELLOW,ORANGE,BROWN,PINK,GRAY1/GREY1,GRAY2/GREY2,GRAY3/GREY3,LIGHTGREEN/LIGHT GREEN,LIGHTBLUE/LIGHT BLUE,BLACK. - Screen/control keys:
HOME,DOWN,UP,LEFT,RIGHT,DEL/DELETE,INS/INSERT,CLR/CLEAR. - Reverse video:
RVS,REVERSE ON,RVS OFF,REVERSE OFF.
- Colors:
- Numeric: e.g.
- Each
- Behaviour notes:
- Token expansion is purely a source transform; once loaded, the program runs exactly as if you had typed the equivalent
CHR$()expressions yourself. - Tokens are only recognised inside quoted strings; everything else in your BASIC code is left untouched.
- Token expansion is purely a source transform; once loaded, the program runs exactly as if you had typed the equivalent
- Coordinate system:
LOCATE col, rowandTEXTAT col, row, textboth use 0‑based screen coordinates, where0,0is the top‑left corner.- Internally these are mapped to ANSI escape sequences as
ESC[row+1;col+1H.
LOCATE:- Moves the cursor without printing: e.g.
LOCATE 10,5positions the cursor at column 10, row 5 before the nextPRINT.
- Moves the cursor without printing: e.g.
TEXTAT:TEXTAT x, y, textmoves the cursor to(x, y)and writestextdirectly at that absolute position.textis any string expression, for example:TEXTAT 0, 0, "SCORE: " + STR$(S).
- Keeping final text visible:
- Terminals often print the shell prompt immediately after your program exits; if your last output is on the bottom row, an implicit newline or prompt can scroll it off‑screen.
- To make sure your final UI remains visible, explicitly position the cursor after your last
TEXTAT/LOCATEoutput, for example:
LOCATE 0, 22
PRINT "PRESS ANY KEY TO CONTINUE";- This ensures the cursor (and thus any following prompt) appears where you expect, rather than accidentally pushing your last line out of view.
The interpreter runs a BASIC source file that contains line‑numbered statements:
10 PRINT "HELLO, WORLD!"
20 END
Run the interpreter and pass the path to your BASIC program:
./basic hello.bas # Linux / macOS / WSL
basic.exe hello.bas # Windows-petscii/--petscii: enable PETSCII/ANSI mode so that certainCHR$control codes (cursor movement, clear screen, colors, etc.) are translated to ANSI escape sequences. Printable and graphics PETSCII codes are mapped to Unicode (e.g. £ ↑ ←, box-drawing, card suits).-petscii-plain/--petscii-plain: PETSCII mode without ANSI: control and color codes output nothing (invisible, like on a real C64), and only printable/graphics bytes produce visible characters. Use this when you need strict one-character-per-column alignment (e.g. viewing .seq files or pasting output into a fixed-width editor).-charset upper|lower: choose the PETSCII character set used for rendering letters in-petsciimode:upper(default): C64 uppercase/graphics character set.lower: C64 lowercase/uppercase character set (useful for.seqart that uses lowercase letters).
-palette ansi|c64: choose how PETSCII colors are mapped (only in-petsciimode):ansi(default): map colors to standard 16-color ANSI SGR codes.c64orc64-8bit: use 8‑bit (38;5;N) color indices chosen to approximate the classic C64 palette. This is most consistent on terminals that support 256 colors.
You can also enable a PETSCII/ANSI mode that understands common C64 control codes inside strings and CHR$ output:
./basic -petscii hello.basIn -petscii mode, CHR$ behaves in a C64-like way: control and color codes are mapped to ANSI escapes, and printable/graphics PETSCII codes (block graphics, arrows, card suits, etc.) are mapped to Unicode so they display sensibly in the terminal.
-
Screen control
CHR$(147): clear screen and home cursor (maps toESC[2J ESC[H]).CHR$(17): cursor down (ESC[B]).CHR$(145): cursor up (ESC[A]).CHR$(29): cursor right (ESC[C]).CHR$(157): cursor left (ESC[D]).CHR$(18): reverse video on (ESC[7m]).CHR$(146): reverse video off (ESC[27m]).CHR$(14): switch to lowercase/uppercase character set (C64 “lowercase mode”).CHR$(142): switch to uppercase/graphics character set (C64 “uppercase mode”).
-
Basic text colors (ANSI approximations of C64 colors)
C64 index ( COLOR/BACKGROUND)PETSCII CHR$()Token(s) you can use in {TOKENS}Approximate colour 0 CHR$(144)BLACKblack 1 CHR$(5)WHITEwhite 2 CHR$(28)REDred 3 CHR$(159)CYANcyan 4 CHR$(156)PURPLEpurple 5 CHR$(30)GREENgreen 6 CHR$(31)BLUEblue 7 CHR$(158)YELLOWyellow 8 CHR$(129)ORANGEorange 9 CHR$(149)BROWNbrown 10 CHR$(150)PINK/ light redlight red 11 CHR$(151)GRAY1/GREY1dark gray 12 CHR$(152)GRAY2/GREY2medium gray 13 CHR$(153)LIGHTGREEN/LIGHT GREENlight green 14 CHR$(154)LIGHTBLUE/LIGHT BLUElight blue 15 CHR$(155)GRAY3/GREY3light gray
If you do not pass a file name, the interpreter will print usage information:
Usage: basic [-petscii] [-petscii-plain] [-palette ansi|c64] <program.bas>
You can use the interpreter for shell-script style tasks:
- Standard input/output
INPUTreads from standard input (one line or token at a time).PRINTwrites to standard output. So you can pipe data in and out:echo 42 | ./basic program.bas./basic program.bas > out.txtErrors and usage go to stderr; only program output goes to stdout when you redirect.
- Command-line arguments
Anything after the script path is available to the program:./basic script.bas first second- In the script:
ARG$(0)= script path,ARG$(1)="first",ARG$(2)="second",ARGC()= 2.
- Running shell commands
SYSTEM("command")runs the command and returns its exit code.EXEC$("command")runs the command and returns its stdout as a string (e.g.PRINT EXEC$("date")).
Example:examples/scripting.basdemonstratesARGC(),ARG$(),SYSTEM(), andEXEC$().
Program text is normalized at load time so compact CBM BASIC without spaces around keywords is accepted. For example: IFX<0THEN, FORI=1TO9, GOTO410, GOSUB5400, and similar forms are rewritten with spaces so the parser recognises IF/THEN, FOR/TO/NEXT, GOTO, and GOSUB. This helps run listings that were saved with minimal whitespace.
Using BASIC-GFX, you can have full PETSCII symbols (upper/lower or upper and graphic) and POKE / PEEK screen memory reading and writing:
(building requires Raylib to be installed - build the graphics target via make basic-gfx)
./basic-gfx examples/gfx_poke_demo.bas./basic-gfx examples/gfx_charset_demo.bas./basic-gfx examples/gfx_key_demo.bas./basic-gfx -petscii examples/gfx_text_demo.bas./basic-gfx -petscii examples/gfx_inkey_demo.bas./basic-gfx -petscii examples/gfx_jiffy_game_demo.bas./basic-gfx -petscii -charset lower examples/gfx_colaburger_viewer.bas— displays.seqart with correct PETSCII rendering (lowercase charset for “Welcome”, “Press”, etc.).
Keyboard polling (basic-gfx):
- BASIC can poll a simple key-down map via
PEEK(56320 + code)where 56320 is 0xDC00. - Supported codes include ASCII
A–Z,0–9, Space (32), Enter (13), Esc (27), and C64 cursor codes Up (145), Down (17), Left (157), Right (29).
INKEY$() (basic-gfx):
INKEY$()is non-blocking: it returns a 1-character string for the next queued keypress, or""if none.- This is driven by the raylib host; it is currently available in
basic-gfx(gfx build) and returns""in the terminal build. - Example:
./basic-gfx -petscii examples/gfx_inkey_demo.bas
TI / TI$ (basic-gfx):
TIis a 60 Hz “jiffy” counter (like C64 BASIC), incremented by the gfx host each frame and wrapping every 24 hours (246060*60 = 5184000).TI$is a string formatted asHHMMSS.- Terminal build: currently not driven by a host tick source (use
SLEEPor your OS clock instead).
SCREENCODES ON|OFF (basic-gfx):
- In gfx builds,
SCREENCODES ONmakesPRINTtreat bytes as PETSCII from.seqstreams; they are converted to C64 screen codes for display (same semantics asCHR$/terminal PETSCII). - This is required for
.seqart viewers such asexamples/gfx_colaburger_viewer.bas. - Use
SCREENCODES OFFto restore the default (ASCII strings likePRINT "HELLO"map naturally). - The window closes automatically when the program reaches
END.
The examples folder (included in release archives) contains:
dartmouth.bas: classic Dartmouth BASIC tutorial; exercisesPRINT,INPUT,IF,FOR/NEXT,DEF FN,READ/DATA, and more.trek.bas: Star Trek–style game; exercisesGET,ON GOTO/GOSUB, multi-dimensional arrays, and PETSCII-style output.chr.bas: PETSCII/ANSI color and control-code test (run with-petscii).examples/testdef.bas,tests/read_data.bas: small regressions forDEF FNandREAD/DATA.test_dim2d.bas,test_get.bas: multi-dimensional arrays andGETEnter handling.tests/fileio.bas,tests/fileeof.bas,tests/test_get_hash.bas: file I/O regression tests.examples/fileio_basics.bas: write and read a file (OPEN, PRINT#, INPUT#, CLOSE) with step-by-step comments.examples/fileio_loop.bas: read until end of file using the ST status variable (ST=0/64/1).examples/fileio_get.bas: read one character at a time with GET#.examples/colaburger_viewer.bas,examples/gfx_colaburger_viewer.bas, andexamples/colaburger.seq: PETSCII .seq file viewer.- .seq files are sequential dumps of PETSCII screen codes (e.g. from BBS logs or PETSCII art).
- The terminal viewer reads the file byte-by-byte with
GET#, prints each byte viaCHR$, and wraps after 40 visible columns (only printable/graphics bytes advance the cursor; control/color codes do not). - Terminal: run with
-petscii-plainso control and color codes output nothing and alignment matches a real C64 screen:
./basic -petscii-plain examples/colaburger_viewer.bas - Graphics (
basic-gfx): run with-petscii -charset lowerfor full PETSCII rendering in a raylib window:
./basic-gfx -petscii -charset lower examples/gfx_colaburger_viewer.bas - With
-petsciiyou get ANSI colors and cursor codes; with-petscii-plainyou get strict character alignment and no escape sequences, ideal for art and fixed-width paste.
examples/scripting.bas: shell-scripting style — command-line arguments (ARGC(),ARG$(0)…ARG$(n)), running commands (SYSTEM("date"),EXEC$("whoami")). Run:./basic examples/scripting.bas [name].guess.bas,adventure.bas,printx.bas, and others for various features.
- Line numbers are required. Any line without a leading integer line number will be rejected when loading.
- Statement separator: use
:to place multiple statements on one line:
10 A = 1 : B = 2 : PRINT A+B
- Comments:
REM comment text' comment text
- Conditionals:
- Full relational operators:
<,>,=,<=,>=,<>. - String and numeric comparisons are both supported.
- You may
THENjump to a line number (IF A>10 THEN 100) or execute inline statements afterTHEN.
- Full relational operators:
- Random numbers:
RND(X)behaves like classic BASIC; a negative argument reseeds the generator.
This README describes the current feature set of the interpreter as implemented in basic.c and is subject to change without notice.
You can either use the provided Makefile (recommended) or compile manually.
- A C compiler with C99 (or newer) support:
- Linux / WSL:
gccorclang - macOS: Xcode Command Line Tools (
clang) - Windows:
- MSVC (Visual Studio / Build Tools), or
- MinGW‑w64 (
gcc)
- Linux / WSL:
- Optional (for raylib-based graphics builds):
- Linux / WSL: install the raylib development package so headers and
pkg-configmetadata are available, for example:- Debian/Ubuntu (if available):
sudo apt-get install -y libraylib-dev - If your distro does not provide
libraylib-dev, build and install raylib from source (summary):- Install deps:
sudo apt-get install -y build-essential cmake git pkg-config libasound2-dev libx11-dev libxrandr-dev libxi-dev libxinerama-dev libxcursor-dev libgl1-mesa-dev libglu1-mesa-dev - Build/install:
git clone https://github.com/raysan5/raylib.git && cd raylib && cmake -B build -DBUILD_SHARED_LIBS=ON && cmake --build build --config Release && sudo cmake --install build - If you installed to
/usr/local/lib, you may need:echo '/usr/local/lib' | sudo tee /etc/ld.so.conf.d/raylib.conf && sudo ldconfig
- Install deps:
- Debian/Ubuntu (if available):
- macOS (Homebrew):
brew install raylib
- Linux / WSL: install the raylib development package so headers and
From the project root:
makeThe Makefile will detect your platform (Linux, macOS, or Windows via MinGW/Cygwin) and produce the appropriate native binary:
- Linux / WSL / macOS:
basic - Windows (MinGW / Cygwin):
basic.exe
To clean the build:
make cleanIf you prefer to invoke the compiler directly:
- Linux / WSL
or
gcc -std=c99 -Wall -O2 basic.c -lm -o basic
clang -std=c99 -Wall -O2 basic.c -lm -o basic
- macOS
Install the Xcode command line tools if you have not already:
Then build with:
xcode-select --install
clang -std=c99 -Wall -O2 basic.c -lm -o basic
- Windows (MSVC)
From a “Developer Command Prompt for VS”:
This will produce
cl /std:c11 /W4 /O2 basic.c
basic.exe. - Windows (MinGW‑w64)
In a MinGW‑w64 shell:
gcc -std=c99 -Wall -O2 basic.c -lm -o basic.exe
The original idea was based on a BASIC project by David Plummer. His video is worth watching:

