A complete Commodore 64 BASIC to JVM bytecode compiler written in Java by Mario Gianota.
Compiles C64 BASIC source programs (.bas) into standard Java .class files that run on any JVM. Every compiled program is a single self-contained .class file with no external runtime dependency — no classpath configuration, no JAR bundling, no library files to distribute.
java BasicC fizzbuzz.bas
java Fizzbuzz
The compiler implements the complete Commodore 64 BASIC V2 specification.
| Type | Syntax | JVM mapping |
|---|---|---|
| Double-precision numeric | A, BC, X1 |
static double field |
| Integer numeric | A%, I% |
static int field |
| String | A$, NAME$ |
static String field |
| 1-D numeric array | DIM A(10) |
static double[] field |
| 1-D integer array | DIM A%(10) |
static int[] field |
| 1-D string array | DIM N$(20) |
static String[] field |
| 2-D numeric array | DIM A(5,5) |
static double[][] field |
All variables are global — C64 BASIC semantics faithfully preserved.
| Statement | Description |
|---|---|
LET var = expr |
Assignment (LET is optional) |
PRINT expr [; expr] [, expr] |
Output — ; suppresses newline, , tabs to next zone |
PRINT# n, expr |
Write to logical file n |
INPUT [prompt;] var |
Read from keyboard |
INPUT# n, var |
Read from logical file n |
GET var$ |
Read single character without Enter |
IF expr THEN stmt |
Conditional — expr, line number, or statement |
GOTO n |
Unconditional jump to line n |
GOSUB n / RETURN |
Subroutine call and return |
ON expr GOTO n1, n2, ... |
Computed goto |
ON expr GOSUB n1, n2, ... |
Computed gosub |
FOR var = start TO limit [STEP s] |
Loop initialisation |
NEXT [var] |
Loop increment and test |
DIM var(n) / DIM var(m,n) |
Array declaration |
DATA val, val, ... |
Compile-time data pool |
READ var [, var ...] |
Read next DATA value |
RESTORE |
Reset DATA read pointer |
DEF FN name(param) = expr |
Define single-argument function |
OPEN n, device, channel, "filename" |
Open logical file |
CLOSE n |
Close logical file |
CMD n |
Redirect PRINT output to logical file n |
POKE addr, val |
Write byte to 64KB memory map |
SYS addr |
Call machine code address |
REM text |
Comment |
END / STOP |
Terminate program |
Mathematical: ABS ATN COS EXP INT LOG RND SGN SIN SQR TAN
String: ASC CHR$ LEFT$ LEN MID$ RIGHT$ STR$ VAL
Memory: PEEK(addr) — reads byte from 64KB memory map as unsigned double (0–255)
User-defined: FN name(expr) — calls a function defined with DEF FN
OPEN 1, 8, 1, "DATA.TXT,S,W" ' sequential write (create/overwrite)
OPEN 1, 8, 1, "DATA.TXT,S,A" ' sequential append
OPEN 2, 8, 2, "DATA.TXT,S,R" ' sequential read
OPEN 2, 8, 2, "DATA.TXT" ' sequential read (default)The compiler maintains a 65,536-byte memory array simulating the C64 address space. Special addresses trigger side effects on the terminal:
| Address | Effect |
|---|---|
POKE 53281, n |
Set screen background colour (ANSI escape) |
POKE 53280, n |
Set border colour (ANSI background) |
POKE 646, n |
Set text/cursor colour (ANSI foreground) |
SYS 64738 |
Cold reset → System.exit(0) |
SYS 58634 |
Clear screen → ANSI ESC[2J |
The compiler is a two-pass design targeting the Jasmin Java assembler as its backend.
BASIC source (.bas)
│
▼
┌───────┐
│ Lexer │ Case-insensitive tokeniser. Handles REM, string literals,
└───────┘ string-function keywords (CHR$, LEFT$...), two-char operators.
│ List<Token>
▼
┌────────┐
│ Parser │ Pass 1 — one forward scan builds six tables:
└────────┘
│ LineNumberTable — every line number → its token sequence
│ VariableTable — all scalar names and types
│ ArrayTable — all array names, dimensions, element types
│ DataTable — all DATA values in program order
│ FnTable — all DEF FN definitions
│ GosubTable — all GOSUB target line numbers
▼
┌─────────────────────┐
│ JasminCodeGenerator │ Pass 2 — generates Jasmin assembly (.j file)
└─────────────────────┘
│ .j file
▼
┌──────────────────┐
│ Jasmin assembler │ External tool (jasmin.jar)
└──────────────────┘
│ .class file
▼
java ClassName
Pass 1 sees the entire program before a single byte of output is generated. This eliminates forward-reference problems entirely — GOTO 500 on line 10 resolves cleanly even if line 500 comes later, because the complete LineNumberTable exists before code generation begins.
ClassName.class
├── static double/int/String fields (one per scalar variable)
├── static array fields (one per DIM'd array)
├── static byte[] __MEM (65,536-byte C64 memory map)
├── static PrintWriter[] __FILE_OUT (file output table, 16 slots)
├── static BufferedReader[] __FILE_IN (file input table, 16 slots)
├── static int __CMD_FILE (CMD redirect target, -1 = screen)
├── static String[] __DATA (DATA pool, if DATA statements present)
├── static int __DATA_PTR (DATA read pointer)
│
├── <clinit>()V initialises all arrays, memory map, DATA pool
├── main(String[])V compiled BASIC program body
├── sub_N()V one method per GOSUB target line number
├── fn_X(D)D one method per DEF FN definition
│
└── bas_*() runtime library (30+ private static methods)
bas_print_d / bas_print_s / bas_println (CMD-aware)
bas_print_comma / bas_println_file
bas_input_d / bas_input_s
bas_get_s
bas_open / bas_close / bas_cmd
bas_print_file_s / bas_print_file_d / bas_println_file
bas_input_file_s / bas_input_file_d
bas_read_d / bas_read_s
bas_abs / bas_atn / bas_cos / bas_exp / bas_int
bas_log / bas_rnd / bas_sgn / bas_sin / bas_sqr / bas_tan
bas_len / bas_asc / bas_chr / bas_left / bas_right
bas_mid / bas_str / bas_val / bas_not
bas_peek / bas_poke / bas_sys / bas_c64_colour
Every BASIC program compiles into a single main method. The JVM JIT compiler treats this as one hot compilation unit and applies its most aggressive optimisations across the entire program. There is no object allocation, no garbage collection pressure, no virtual dispatch — all variable access is getstatic/putstatic on static fields and all calls are invokestatic. Compiled C64 BASIC programs may run faster than equivalent Java spread across many methods and objects.
| File | Lines | Purpose |
|---|---|---|
TokenType.java |
~90 | Enum of every token category |
Token.java |
~110 | Immutable token value object |
Lexer.java |
~380 | Tokeniser |
Tables.java |
~420 | Six Pass 1 data structures |
Parser.java |
~320 | Pass 1 — builds all six tables |
ExpressionParser.java |
~380 | Recursive descent expression parser |
RuntimeLibrary.java |
~1000 | Jasmin source templates for all bas_* methods |
JasminCodeGenerator.java |
~1700 | Pass 2 — generates Jasmin assembly |
BasicCompiler.java |
~130 | Coordinates all stages |
BasicC.java |
~190 | Command-line driver |
CompilerDemo.java |
~250 | Test suite — 25 programs |
- Java 8 or later (tested on Java 21)
- Jasmin assembler (
jasmin.jar)
git clone https://github.com/yourusername/c64-basic-compiler.git
cd c64-basic-compiler
javac *.javaPlace jasmin.jar in the same directory, or set the JASMIN_JAR environment variable.
java BasicC program.bas
java ProgramThe class name is derived from the filename — hello-world.bas becomes HelloWorld.
Flags:
java BasicC <file.bas> [--jasmin path/to/jasmin.jar] [--keep-j]
--jasmin <path> Path to jasmin.jar
--keep-j Retain the .j intermediate file after assembly
Jasmin is located automatically:
--jasminflagJASMIN_JARenvironment variablejasmin.jarin the current directorylib/jasmin.jaror~/bin/jasmin.jar
java CompilerDemo # compile all 25 test programs to .j files
java -jar jasmin.jar Test5FizzBuzz.j
java Test5FizzBuzz10 PRINT "HELLO, WORLD!"
20 END10 FOR I = 1 TO 100
20 IF I/3 = INT(I/3) AND I/5 = INT(I/5) THEN PRINT "FIZZBUZZ" : GOTO 60
30 IF I/3 = INT(I/3) THEN PRINT "FIZZ" : GOTO 60
40 IF I/5 = INT(I/5) THEN PRINT "BUZZ" : GOTO 60
50 PRINT I
60 NEXT I10 DIM A(4)
20 A(0)=5 : A(1)=3 : A(2)=8 : A(3)=1 : A(4)=4
30 FOR I = 0 TO 3
40 FOR J = 0 TO 3 - I
50 IF A(J) <= A(J+1) THEN GOTO 80
60 LET T = A(J) : A(J) = A(J+1) : A(J+1) = T
80 NEXT J
90 NEXT I
100 FOR I = 0 TO 4 : PRINT A(I) : NEXT I10 DATA "MERCURY","VENUS","EARTH","MARS","JUPITER"
20 FOR I = 1 TO 5
30 READ P$
40 PRINT P$
50 NEXT I10 DEF FN SQ(X) = X * X
20 DEF FN CUBE(X) = X * X * X
30 FOR I = 1 TO 5
40 PRINT FN SQ(I), FN CUBE(I)
50 NEXT I10 OPEN 1, 8, 1, "PLANETS.TXT,S,W"
20 DATA "MERCURY","VENUS","EARTH","MARS"
30 FOR I = 1 TO 4
40 READ P$
50 PRINT# 1, P$
60 NEXT I
70 CLOSE 1
80 OPEN 2, 8, 2, "PLANETS.TXT,S,R"
90 FOR I = 1 TO 4
100 INPUT# 2, P$
110 PRINT P$
120 NEXT I
130 CLOSE 210 FOR X = 1 TO 3
20 ON X GOSUB 100, 200, 300
30 NEXT X
40 END
100 PRINT "ONE" : RETURN
200 PRINT "TWO" : RETURN
300 PRINT "THREE" : RETURNCopyright © 2024–2026 Mario Gianota. All rights reserved. Contact: mariogianota@protonmail.com
Non-commercial use is free, provided this copyright notice and the following attribution are preserved in all copies and derivative works:
Original author: Mario Gianota (mariogianota@protonmail.com)
Commercial use requires a written licence agreement. No fee is payable until annual gross revenue attributable to this software exceeds USD $10,000.
See LICENCE for the complete terms and conditions.