A floating point BASIC interpreter for the 6502 microprocessor, targeting the Apple II and the sim6502 simulator, with the capability of being extended to other platforms.
To build and test the project, you need the following tools in your PATH:
- cc65 compiler package: Specifically
cl65(the compiler/linker) andsim65(the sim6502 simulator). - make: For automating the build process.
- m4: A macro processor used to generate constants and zero-page definitions.
- expect: Used for running automated integration tests.
- python: For running the script that generates the lexer data, and other project utilities.
In addition, the project requires a Python environment in .venv. To create, run:
python -m venv .venv
Sometimes the local Python might be called python3 instead of python.
The project uses a Makefile to manage the build process.
- Build all targets:
make - Run unit tests:
make test - Run integration tests:
make expect_test
The project uses .m4 files to ensure consistency across assembly, C, and include files.
constants.m4contains constant values. It is processed bym4to generateconstants.inc(assembly) andconstants.h(C).zeropage.m4contains variables stored in zero page. It is processed to generatezeropage.s(the actual ZP definitions),zeropage.inc(global declarations for assembly), andzeropage.h(C headers).
The simulation version can be run directly from the command line:
sim65 basic_sim6502The file basic_apple2 is an AppleSingle-format executable. To run it:
- Create an Apple II disk image (DOS 3.3). You can use a tool like AppleCommander:
java -jar ac.jar -dos140 basic.dsk
- Add the
basic_apple2file to the disk image. Use-asbecause this is an AppleSingle file.java -jar ac.jar -as basic.dsk basic < basic_apple2 - Boot a DOS 3.3 disk in an emulator, insert the BASIC disk, and run it using
BRUN BASIC(orBRUN BASIC,D2if you putbasic.dskin the second drive.) If you don't have an emulator, try the one at apple2ts.com.
Instead of creating a new disk image, you can duplicate an existing DOS 3.3 disk image (search around for "blank DOS 3.3 boot disk" or something like that), then you can boot and run BASIC from the same disk.
To run on real hardware, you obviously need to put basic.dsk on a physical disk, or on a disk emulator like
a Floppy Emu. If you have an actual Apple II then you presumably understand
how to do this. I've only tested on my Apple II+, so if it doesn't work on your e/c/gs, let me know.
The basic_apple1 binary targets the original Apple 1 and compatible hardware including the
Replica-1, APL1,
and most Apple 1 emulators. All of these use the original Apple 1 PIA I/O at $D010–$D013. The binary
loads at $4000.
-
Build the binary and WozMon text file:
make build/basic_apple1.txt
This creates
build/basic_apple1.txtcontaining WozMon-formatted hex load text at address$4000.Alternatively, you can build
build/basic_apple1and convert the raw binary using bin2woz:bin2woz -a 0x4000 build/basic_apple1 > build/basic_apple1.txtEach line of the output contains a 4-digit hex address followed by up to 16 bytes, ready to be pasted into WozMon or sent via the APL1 Terminal's Send Program panel.
-
Load the program into your Apple 1 (or emulator) using WozMon by pasting the contents of
build/basic_apple1.txt. -
Run it:
4000R
The basic_ac6502 binary targets the ac6502 computer system and runs as a 32 KB cartridge image overlaying $C000–$FFFF.
-
Install the emulator: Install Node.js (e.g., via Homebrew with
brew install node) and install theac6502emulator package globally:npm install -g ac6502
-
Obtain the BIOS ROM: The emulator requires the system BIOS ROM (
BIOS.bin), which can be obtained from the 6502-BIOS repository on GitHub. -
Run the cartridge:
ac6502 -r /path/to/BIOS.bin -c build/basic_ac6502
The interpreter manages memory using several zero-page pointers:
program_ptr: Points to the start of the BASIC program.- Program structure: Lines are stored sequentially. Each line record starts with a 1-byte size, followed by a 2-byte line number. Statements within the line begin with an offset to the next statement and end with
0. The program ends with a "null line" (size 0).
- Program structure: Lines are stored sequentially. Each line record starts with a 1-byte size, followed by a 2-byte line number. Statements within the line begin with an offset to the next statement and end with
variable_name_table_ptr: Points to the start of the Variable Name Table (VNT), which immediately follows the program.- VNT structure: Each record starts with a size byte (MSB set if 2 bytes). The variable name follows, with the MSB set on the last character. String variables end with
$. The variable value is stored after the name. A zero-size record terminates the table.
- VNT structure: Each record starts with a size byte (MSB set if 2 bytes). The variable name follows, with the MSB set on the last character. String variables end with
array_name_table_ptr: Points to the Array Name Table (ANT) following the VNT.- ANT structure: Similar to VNT, but after the name, it contains a 1-byte arity (dimensions) followed by words defining the element size at each level for offset calculation.
free_ptr: Points to the first byte of free memory after the ANT.string_ptr: Points to the bottom of the string space. This space grows downwards fromhimem_ptrand is compacted upwards during garbage collection.himem_ptr: The highest address used by the interpreter and the ceiling for the string space.
The parser converts user input into a tokenized program in two stages:
- DFA Lexer: A dedicated lexer (
lexer.s) processes raw input using DFA state tables generated from regexes bygenerate_lexer_data.py. It handles case folding and converts keywords into single-byte tokens. - Parser Virtual Machine (PVM): An LL(1) predictive recursive-descent parser (
parser.s) that validates statement and expression grammar deterministically with single-token lookahead and without backtracking.
- Objective: Detect syntax errors up-front and replace keywords with 1-byte tokens for compact storage and efficient execution.
- Type checking: Notably, the parser does not perform type checking; this is handled at runtime.
- LIST command: Handles the reverse process, expanding tokens back into human-readable code.
The interpreter uses two stacks for expression evaluation and flow control:
- Primary stack: Holds intermediate numerical and string values, as well as the
control structure used for
GOSUBANDFOR. ThePOPcommand removes one control structure from this stack. - Operator stack: Holds pending operators to respect precedence.
Most statements and functions are implemented by pushing values onto the primary stack and popping them to perform operations.
VC83 BASIC uses a custom 5-byte floating point format documented in fp.s:
-
Format:
sttttttt tttttttt tttttttt tttttttt eeeeeeee-
s: Sign bit -
t: 32-bit fractional significand encoded as 31 bits with implied1. -
e: 8-bit exponent, excess-127 (127 =$2^0$ )
-
-
Registers: The system uses two main floating point registers stored in zero page,
FP0andFP1.FPXextends theFP0significand to 64 bits. -
Operations:
-
Unary functions (e.g.,
SIN,LOG,NEG) always operate onFP0. -
Binary functions (e.g.,
FADD,FMUL) operate onFP0and an "argument" value. The address of the argument is passed inAYand loaded intoFP1before the operation.
-
Unary functions (e.g.,
While VC83 BASIC uses the same number of bits to represent a floating point value as Microsoft BASIC, note that the implied 1 digit is to the left of the binary point (like IEEE-754), vs. Microsoft which places it to the right of the binary point.
The floating point system does not support subnormal values, NaN, or infinity.
Strings in VC83 BASIC are stored with the following structure:
- Layout:
[Length Byte] [String Data...] [Extra Byte 1] [Extra Byte 2] - Size: The
Length Byteand the two extra bytes are not included in the reported length of the string. - Allocation: The interpreter creates new strings by moving
string_ptrdown and writing the new string at the newstring_ptrlocation. Thus,string_ptralways points to the most recently created string. - Garbage collection: When
string_ptrreachesfree_ptr, the interpreter triggers a garbage collector.- The collector moves all still-referenced strings to the top of the string space (towards
himem_ptr). - During collection, the two extra bytes following each string are used to store a forwarding address.
- The collector moves all still-referenced strings to the top of the string space (towards
Located in the tests/ directory (e.g., fp_test.c). These tests are written in C but interface with the 6502 assembly code through c_wrappers.s, which provides a C-callable interface to assembly functions. They are run using sim65.
Located in expect_tests/. These are integration tests that use the expect tool to feed BASIC commands into sim65 basic_sim6502 and verify the output. This ensures the interpreter behaves correctly from a user's perspective.
VC83 BASIC has a few improvements over Microsoft BASIC:
- Variable names: Variable names can be any length.
- String GC: The string garbage collector is much more efficient.
However, VC83 BASIC is also quite a bit slower than Microsoft BASIC. This is an area for development; it doesn't seem like it should be an unfixable problem.
My goal was to fit the BASIC core into 8K. But in order to get there, I had to remove platform-specific features such as I/O and graphics and sound statements from the core. So the BASIC interpreter that will actually run on real hardware will probably be 10K, 12K, or even 16K.
VC83 BASIC does not support for DEF FN or ON ERROR. Let me know if these are important.
To add support for a new hardware platform:
- Linker config: Create an
ld65configuration file (e.g.,{platform}/{platform}.cfg). - Initialization: Implement platform-specific startup and mandatory I/O (
getch,putch,inkey,readline,newline,tab,save,load) in its own directory. On failure, I/O routines should invokeraise ERR_IO_ERROR. - Master assembly file: Create a
basic_{platform}.sfile that.includesbasic.incand all your platform-specific assembly files. - Makefile: Add the new target to the
TARGETSlist in theMakefileand define the build rules. - Extensions (optional): Implement platform-specific extension macros in
{platform}.inc(extension_statement_keywords,extension_pvm_statements,extension_pvm_code, split vectorsextension_statement_vectors_l/h, andextension_statement_flags) and handler code in{platform}_extension.s. Seeac6502.inc/ac6502_extension.sorapple2_lc.inc/apple2_extension_lc.sfor examples.
VC83 BASIC is available to you under the terms of the MIT License. You're welcome to use it with or without changes in your own projects, provided you adhere to the license terms.
The VC83 name itself and logo are restricted. You can share the official version, but forks must be rebranded.
Contributions are welcome! Please keep the following in mind:
- Licensing: By contributing code to this project, you agree to license your contribution under the MIT License.
- Pull requests: Pull requests are welcome, but I can't guarantee that I'll merge them. To improve the chance of your contribution being accepted, please reach out or open an issue to discuss your proposed changes before starting work.
