name: prog8-coder description: Write Prog8 programs (6502-targeted language with 8-bit retro systems as targets) license: MIT compatibility: opencode
Prog8 Coder Skill
You are working with Prog8 source code (.p8 files) or its Intermediate Representation (.p8ir files). Prog8 targets 8-bit systems (C64, CX16, C128, PET32) with the 6502 CPU, plus a virtual target for testing.
Follow ALL the rules below carefully.
General & Setup
- Git operations: When moving, renaming, or deleting git-tracked files, always use
git mvorgit rminstead of plainmv/rm. This preserves history and properly stages the change. Plainmv/rmcauses git to see them as delete+add (losing history). - A program = a
mainblock containing astartsubroutine entry point, plus optional other subroutines/blocks - Add
%zeropage basicsafeat the top of your program to allow clean return on exit (instead of resetting the machine/emulator) - Module imports:
%import modulename— noasaliasing. Use the module's defined prefix (e.g.,%import textio→txt.xxx) - Virtual target for testing: Use
prog8c -target virtual -emu input.p8orprog8c -vm input.p8irfor fast testing - Fast syntax check:
prog8c -check input.p8— does NOT produce output files (.prg, .asm, etc.), just errors - Output directory:
prog8c -out outdir input.p8(default: same dir as source) - Compilation outputs:
*.prg(program binary),*.asm(assembly listing),*.list(full listing),*.p8ir(IR for VM),*.vice-mon-list(Vice debug symbols).p8irfiles contain the Intermediate Representation — a sequence of named chunks, each containing typed instructions and virtual registers. This is a target-independent representation of the program, executable by the built-in Virtual Machine viaprog8c -vm file.p8ir. Useful for debugging the compiler's code generation path without involving 6502 assembly.
- Debugging switches:
-noopt(disable optimizations),-printast1(parsed AST),-printast2(optimized simple AST),-compareir(compare IR outputs),-dumpsymbols(print all symbols),-dumpvars(print all variables) - Library search (preferred):
prog8c -libsearch <regex>— search for a regex pattern in the embedded library files. Extremely useful to quickly find library routines, variables, strings, or signatures (e.g.,prog8c -libsearch "txt\."lists all textio routines;prog8c -libsearch "sin"finds math functions) - Library dump:
prog8c -libdump <dir>— extract all embedded library source files into a directory for direct inspection (less common, use-libsearchfirst) - Other useful flags:
-quiet(suppress messages),-warnimplicitcasts(warn on implicit type widening),-daemon(keep a background compiler process alive to speed up multiple successive compilations — must be passed on everyprog8cinvocation) - Test programs: add
%zeropage basicsafeand%option no_sysinitat top - Compiler unit test snippets: These are prog8 code snippets embedded in the Prog8 compiler's own Kotlin unit tests (e.g., in
TestAstChecks.kt,TestOptimization.kt, etc.). They are NOT standalone Prog8 programs:- Keep them self-contained — avoid
%importdirectives so they don't depend on library files (the test setup may not have the library search path configured) - Use the
cx16target (notvirtual, unless specifically testing the VM) - Consider setting
writeAssembly=falseif the test only needs to check the generated AST (much faster) - Use
optimize=falseby default for these snippets - Do NOT use the
%encoding iso/txt.iso()/sys.poweroff_system()pattern in these snippets — that's only for real CX16 emulator runs
- Keep them self-contained — avoid
sysmodule: always available, no import needed- CX16 debugging: Add
%encoding iso, calltxt.iso()instart(), end withsys.poweroff_system(). For emulator:x16emu -echo iso -run -prg input.prg 2>&1 | grep ... - CX16 memory map:
Bank registers (in zeropage):Bank Offset Content $0000-$9EFF Fixed/System RAM $9F00-$9FFF I/O Area (VIA, VERA, YM2151) $00-$FF:$A000-$BFFF Banked RAM (max 2 MiB, often 512 KiB) (256x8K banks) $00-$1F:$C000-$FFFF System ROM (512 KiB) (32x16K banks)$00selects the current RAM bank (0-255),$01selects the current ROM bank (0-31). For JSRFAR/banked calls from Prog8, useextsub @bank ...instead of manipulating these directly. Thecx16module providescx16.get_ram_bank()/cx16.set_ram_bank()andcx16.get_rom_bank()/cx16.set_rom_bank()— prefer these over directly poking$00/$01. Full hardware specs at https://ayce.dev/emptyx16.html#emptyx16---x16-hardware-specs
Datatypes & Variables
- Primitives:
bool,byte,ubyte,word,uword,long,float,str ubyte/uword= unsigned;long= signed 4-byte;float= 5-byte MS format;str= 0-terminated ubytes (max 255 chars)- float requires
%import floatsat top of file, else compiler errors - Arrays: max 256 bytes (512 for split word arrays). For larger data, use
memory()+ pointers memory(name, size)returns auwordaddress to a statically reserved block of memory- To point a typed pointer at a
memory()block, assign the uword directly:^^MyStruct ptr = memory("name", size). The^^Type:expressionsyntax (below) only works with array literals, not general uword expressions. - Struct initialization:
^^StructType ptr = ^^StructType:[val1,val2,...](the^^StructType:can be omitted if inferable). This^^Type:[...]syntax does NOT work with variables ormemory()— only literal arrays. - Struct definitions must be inside a block, not at file level.
- Struct fields: only simple types +
strallowed. NO arrays as fields.strin struct =^^ubyte - Word/pointer arrays split into LSB/MSB by default. Override with
@nosplit - No call stack: all variables statically allocated. No recursion without manual stack management
- Variables zero-initialized (globals at start, locals on subroutine entry)
@shared,@zp,@requirezp,@dirty,@nosplitare tags that go on variable declarations. Place them after the datatype (and array specifiers), before the variable name(s):
The grammar is:private ubyte[8] @shared vera_storage ; single var with tag ubyte @zp @shared varname ; multiple tags allowed ubyte @requirezp var1 ; require zeropage ubyte[] @shared names ; array with tag[private] datatype [arraydims] [tags...] identifierlist@sharedmarks variables as "used by external code" (assembly), prevents the optimizer from removing them@zp/@requirezp: use sparingly — only for pointers (limited zeropage space)- Pointer-like typed pointers (
^^type) support C-style scaled arithmetic;uwordpointers always treat element as 1 byte &= untyped address (uword);&&= typed pointer- Available zeropage scratch:
P8ZP_SCRATCH_B1,P8ZP_SCRATCH_REG,P8ZP_SCRATCH_W1,P8ZP_SCRATCH_W2,P8ZP_SCRATCH_PTR— and cx16 virtual registers R0-R15 on all targets - Virtual registers (
cx16.r0–cx16.r15): global 16-bit, NOT preserved across calls. - WARNING: Virtual registers in ISR/IRQ handlers: The virtual registers R0-R15 are not preserved across the IRQ handler call. If your handler uses them, it will corrupt the interrupted program's state. Either avoid using them in the handler, or save/restore with
cx16.save_virtual_registers()/cx16.restore_virtual_registers(). This applies to all targets, not just CX16. - WARNING: Long operations clobber R12-R15: Some operations on
longvalues use R12-R15 as temporary storage and will silently overwrite them. Do not rely on R12-R15 values when working with longs, and avoid using R12-R15 explicitly if your code uses long arithmetic. - WARNING: VERA registers in ISR handlers (CX16): If your IRQ handler reads or writes VERA control registers (e.g.,
cx16.VERA_DATA0,cx16.VERA_ADDR_L, etc.), you must save and restore the VERA context around the handler's work usingcx16.save_vera_context()/cx16.restore_vera_context(). Without this, the handler will corrupt any VERA operations (tilemap updates, sprite positioning, etc.) happening in the interrupted main program. - IRQ handler best practices: Keep handlers extremely short and fast — they run with interrupts disabled and steal cycles from the main program. Do NOT do lengthy processing, I/O, or complex subroutine calls inside the handler. Instead, set a boolean flag or semaphore that the main loop checks periodically, and do the actual work there.
- Math performance: integer trig (
math.sin8,math.cos8) uses fast LUTs; float trig (floats.sin/cos) is much slower
Recursion & Stack Management
No call stack for variable storage — recursion overwrites locals. To handle it:
- CPU hardware stack:
push()/pushw()/pushl()/pushf()andpop()/popw()/popl()/popf(). Save/restore locals around recursive calls - Software stacks:
buffers.stack(uword) /buffers.smallstack(ubyte). Both providepush_b()/push_w()andpop_b()/pop_w() - Iterative rewrite (preferred): Many recursive algorithms work as
repeatloops with explicit bounds — avoids all stack overhead
Strings, Arrays & Pointers
str/ array: max 256 bytes.long[]limited to 64 entries (64x4=256).str[]for string arrays:str[5] names = ["a","b","c","d","e"]- 2D arrays:
type[rows][cols] name, accessname[r][c]. Flat init list only (no nested[[...]]). Total size still ≤ 256 bytes - str/array passed as pointer to subroutine (receiving subroutine gets
^^ubyteor^^element) - No const pointers or pointer-to-pointer currently supported
- Parsing limitation:
pointer[index].fieldas assignment target needs^^:pointer[index]^^.field = value - Pre-allocate buffers:
str buffer = "." * 50(empty""allocates nothing —strings.append()will fail) - No reassignment: can't
buffer = "new text"after declaration. Usestrings.copy()/strings.ncopy() - String concat is expensive on 6502. Prefer separate prints over concatenation
- Efficient buffer iteration: prefer
ptr+++@(ptr)over@(buffer + offset). Exception: if offset is aubyte(≤ 255),buffer[offset]works fine @(ptr)= peek/poke byte. For words:peekw/pokew, longs:peekl/pokel, floats:peekf/pokef, bools:peekbool/pokebool- Assigning a uword address to a
strfield of a struct is done by direct assignment. Compute the address in a uword variable first, then assign:cx16.r0 = &namebufs + offset; entry.name = cx16.r0. The^^ubyte:(expr)cast syntax does not parse — use a temp uword instead. - Use
len(array)instead of hardcoded sizes - Array indexing is 0-based:
arr[0]is first element - Static memory only — real dynamic allocation impossible, but can emulate with a simple arena allocator over a
memory()slab
Logic & Control Flow
- Logical operators (short-circuit, bool only):
and,or,xor,not- In
a and b: ifais false,bis NOT evaluated - In
a or b: ifais true,bis NOT evaluated - Important when
bhas side effects
- In
- Bitwise operators:
&,|,^,~,<<,>> - Bit rotation:
rol()/ror()(through carry),rol2()/ror2()(no carry) - CPU status flag branches:
if_cs,if_cc,if_z,if_nz(compile to single 6502 branch instructions) - If-expressions for simple value assignments based on a choice
- Optional braces in if/else: when the
iforelsebody is a single statement, the{ }can be omitted. Place the statement on the next line, indented. Example:if x < 5 txt.print("small") deferdefers statement execution until scope exit. Multiple defers fire in reverse registration order (LIFO / stack order — last deferred runs first). A defer is only registered if execution reaches that statement — conditional paths that skip thedeferline will not register it.goto, labels, jump lists allowed- Common mistake:
and/orfor bitmasking — use&/|instead!
The when Statement
The when statement is a control flow construct that enables you to execute a specific action based on the value of an expression. It is generally more readable and often more efficient than a sequence of if-else if statements, as the compiler can optimize it into more efficient branching structures, such as a jump table.
- Expression: Evaluates an expression and compares it against case values.
- Cases: Defined by a value followed by the
->operator. - Blocks: Use
{ }to enclose multiple statements for a case. - Else Clause: Serves as a default handler; mandatory unless the expression type is fully covered.
- Efficiency: Recommended for handling sets of fixed choices as it typically results in better assembly code.
Loop Constructs
Prog8 supports these loop types. All support break and continue (except unroll).
for loop — iterate over a range or array
- Loop variable must be declared separately before the
forstatement - Works with
ubyte,byte,uword,word,long, pointer types (NOTfloat) - Iterates over ranges (
start to end), descending ranges (start downto end), or arrays/strings - Optional
step <constant>for non-unit step sizes - Loop variable value after the loop is undefined — don't rely on it
- Descending loops with
downtousually produce more efficient 6502 code
ubyte i
for i in 20 to 155 {
; body
break ; exit loop
continue ; next iteration
}
; descending
for i in 155 downto 20 {}
for i in 155 to 20 step -1 {}
; iterate over array elements
uword[] fib = [0, 1, 1, 2, 3, 5, 8, 13]
uword num
for num in fib {
...
}
while loop — repeat while condition is true
while condition {
; body
break
continue
}
do-until loop — always executes body at least once
do {
; body
break
continue
} until condition
repeat loop — repeat a fixed number of times (most efficient)
- Most efficient code generation — prefer over
forwhen loop variable not needed - Omit count for infinite loop (still supports
break)
repeat 15 {
; body
break
continue
}
; infinite:
repeat {
; body
break if x==5
}
unroll loop — compile-time code duplication
- Not a real loop — duplicates body N times at compile time
- No
break/continueallowed - Only simple statements (assignments, calls) in body
- Constant iteration count required
unroll 80 {
cx16.VERA_DATA0 = 255
}
Subroutines & Return Values
- Don't use
privateon subroutines and variables (including nested ones) unless the user asks for it. Everything is public by default in Prog8 — follow that convention. inlinekeyword for subroutines to suggest inlining- No function overloading (except builtins) and no polymorphism in general. This means you must call specific routines for different types (e.g.,
txt.print_ub(val)vstxt.print_w(val)instead of a genericprint(val)). Cannot use builtin names (msw, lsw, msb, lsb, mkword, mklong, peek, peekw, peekl, etc.) as variable/sub names - Can return 0, 1, or multiple values:
a, b, c = routine(). Usevoidto skip:void routine(),a, void, c = routine() - Nested subroutines access parent scope variables directly
Assembly Subroutines (asmsub)
For low-level assembly that gets arguments via registers and returns values in registers.
- Syntax:
[private] [inline] asmsub subname(params) [clobbers(regs)] [-> returns] { %asm {{ ... }} } - Body Restriction: The body of an
asmsubmust only contain a single%asm {{ ... }}node. Regular Prog8 statements or nested blocks are NOT allowed. - Parameters:
type name @register(e.g.,ubyte val @A,uword addr @AX,float f @FAC1). - Return Values:
-> type @register(e.g.,-> ubyte @A,-> bool @Pzfor immediate branch use). - Registers:
- 8-bit:
@A,@X,@Y - 16-bit:
@AX,@AY,@XY(register pairs) - Float:
@FAC1,@FAC2(Floating Point Accumulators) - Virtual:
@R0–@R15(16-bit),@R0R1–@R14R15(32-bit combined) - Status Flags (for returns):
@Pc(Carry),@Pz(Zero),@Pv(Overflow),@Pn(Negative)
- 8-bit:
- Clobbers:
clobbers (A, X, Y)— list all hardware registers modified by the routine. - Parameter names are for documentation and type checking only. Use the registers in your assembly code.
- Inlining:
inline asmsubwill paste the assembly code directly at the call site, avoidingjsr/rtsoverhead.
External Subroutines (extsub)
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
- Syntax:
[private] extsub [@bank <value>] address = subname(params) [clobbers(regs)] [-> returns] - Address: Can be a hex literal (
$C000) or a constant expression. - Bank (optional):
@bank <integer>(constant bank) or@bank <identifier>(variable bank). - No Body: Unlike
asmsub,extsubhas no{ }body; it just maps a signature to an address. - Example:
; CX16 KERNAL CHROUT extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y) ; Routine in a specific RAM bank extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
Assembly Programming Details
- Symbol prefixes:
p8v_(variables),p8s_(subroutines),p8b_(blocks),p8c_(constants),p8l_(labels),p8t_(structs),p8_(other) - Fully qualified names:
p8b_blockname.p8v_varname,p8b_blockname.p8s_subname.p8v_localvar - Within a
.proc, short names often work.%option no_symbol_prefixingdisables prefixes (used bycbm,cx16,txt) - Split word arrays: append
_lsb/_msbto variable name (e.g.,p8v_myarray_lsb) - CX16 target: use 65C02 instructions (STZ, PHX, etc). Others: 6502 only
- Assembly syntax: 64tass.
.proc/.pend,_labelfor locals,.byte/.word/.dwordfor data,=for equates - Instructions like
rol,ror,asl,lsrrequire explicit operand:rol a - Anonymous labels:
+(forward),-(backward), branch with+,++,+++or-,--,--- - Register Aliases: It's common to define aliases at the start of an
asmsub:asmsub my_routine(uword ptr @AX) { %asm {{ ptr_lo = p8zp_scratch_w1 ptr_hi = p8zp_scratch_w1+1 sta ptr_lo stx ptr_hi }} }
Standard Library
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
- Online location: https://prog8.readthedocs.io/en/latest/libraries.html#low-fi-variable-and-subroutine-definitions-in-all-available-library-modules they are linked there 1 for each compilation target
- Structure: builtin functions, then module sections with variables/constants (
type name) and subroutines (name (params) -> returntype)
- Text output:
textiomodule (txt.print,txt.chrout,txt.print_b/_ub/_w/_uw/_l/_bool,txt.print_ffor floats,txt.spc(),txt.nl()). Note: Prog8 has no function overloading, so you cannot usetxt.print(number)— you must call the specific routine for the type (e.g.,txt.print_ub(val)for an unsigned byte). - Math:
mathmodule — integer trig (sin8,cos8) via fast LUTs;math.rnd()for random numbers - String conversion:
convmodule (str_uword,str2word, etc.) — for printing numbers use txt routines instead - Char operations:
stringsmodule (isdigit,isxdigit,isupper,islower,isletter,isspace,isprint) — use these instead of manual ASCII/PETSCII arithmetic - String functions return useful lengths — capture them:
len = strings.copy(dest, src),len = strings.append(buf, text),len = strings.upper(mystr)
Syntax & Formatting
- Hex:
$FF(not0xFF); Binary:%1010(not0b1010). Underscores for readability:25_000_000 - 4-digit hex
$0000= uword. No type suffixes (no0L). Cast:expr as type - Augmented assignment:
+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>= ;starts a comment to end of line — NOT a statement separator. One statement per line- No
elif: use nestedelse { if ... } - Type casting:
expression as type(e.g.,bytevar as word).ashas very low precedence (lower than arithmetic) - No automatic type widening:
byte*byte=byte(overflow possible!). Cast explicitly - No bare
{ }blocks like C/Java - Indentation: 4 spaces for .p8 and .asm files (no tabs)
- Character encoding: 6502 targets use PETSCII by default (call
txt.lowercase()at start for lowercase). Virtual target uses ISO (%encoding iso+txt.iso()) - Array size inferred from initializer:
str[] types = ["a", "b", "c"] - Enums:
Enum::Valuesyntax (double colon), declared inside a block. They are syntactic sugar for a list ofconstdeclarations, not a type. Use enums for related values,constfor standalone - Avoid
globals.XXXX— move constants closer to where they're used - Member access through pointers: use
.for both direct and pointer access. The compiler infers the type. For complex assignment targets,^^may be needed:ptr^^.field = value - Qualified names: must use full path from top level (e.g.,
cx16.r0, not relative)