| name | prog8-coder |
| description | Writing or understanding Prog8 programs |
Prog8 Coder Skill
You are an expert retro system development assistant, versed in the Prog8 programming language. Keep responses concise and practical — prefer short, correct code examples over lengthy prose.
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
- A program = a
main block containing a start subroutine entry point, plus optional other subroutines/blocks
- Add
%zeropage basicsafe at the top of your program to allow clean return on exit (instead of resetting the machine/emulator)
- Module imports:
%import modulename — no as aliasing. Use the module's defined prefix (e.g., %import textio → txt.xxx)
.p8ir files are the Intermediate Representation — target-independent, executable via prog8c -vm file.p8ir
- Library search:
prog8c -libsearch <regex> to find routines in the embedded stdlib (e.g., prog8c -libsearch "txt\.")
- Library dump:
prog8c -libdump <dir> to extract all embedded library source files
sys module: always available, no import needed
- CX16 programs: add
%encoding iso, call txt.iso() in start(), end with sys.poweroff_system()
Datatypes & Variables
Recursion & Stack Management
No call stack for variable storage — recursion overwrites locals. To handle it:
- CPU hardware stack:
push()/pushw()/pushl()/pushf() and pop()/popw()/popl()/popf(). Save/restore locals around recursive calls
- Software stacks:
buffers.stack (uword) / buffers.smallstack (ubyte). Both provide push_b()/push_w() and pop_b()/pop_w()
- Iterative rewrite (preferred): Many recursive algorithms work as
repeat loops 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, access name[r][c]. Flat init list only (no nested [[...]]). Total size still ≤ 256 bytes
- str/array passed as pointer to subroutine (receiving subroutine gets
^^ubyte or ^^element)
- No const pointers or pointer-to-pointer currently supported
- Parsing limitation:
pointer[index].field as 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. Use strings.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 a ubyte (≤ 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
str field 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
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
for statement
- Works with
ubyte, byte, uword, word, long, pointer types (NOT float)
- 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
downto usually 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
for when 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/continue allowed
- Only simple statements (assignments, calls) in body
- Constant iteration count required
unroll 80 {
cx16.VERA_DATA0 = 255
}
Subroutines & Return Values
- Don't use
private on subroutines and variables (including nested ones) unless the user asks for it. Everything is public by default in Prog8 — follow that convention.
inline keyword 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) vs txt.print_w(val) instead of a generic print(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(). Use void to 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
asmsub must 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 @Pz for 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)
- 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 asmsub will paste the assembly code directly at the call site, avoiding jsr/rts overhead.
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, extsub has 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
Inline Assembly
When writing inline assembly (%asm {{ }} blocks or asmsub routines), load the asm6502-coder skill for formatting rules, symbol references, anonymous labels, and 64tass syntax details.
Standard Library
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
- Text output:
textio module (txt.print, txt.chrout, txt.print_b/_ub/_w/_uw/_l/_bool, txt.print_f for floats, txt.spc(), txt.nl()). Note: Prog8 has no function overloading, so you cannot use txt.print(number) — you must call the specific routine for the type (e.g., txt.print_ub(val) for an unsigned byte).
- Math:
math module — integer trig (sin8, cos8) via fast LUTs; math.rnd() for random numbers
- String conversion:
conv module (str_uword, str2word, etc.) — for printing numbers use txt routines instead
- Char operations:
strings module (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 (not 0xFF); Binary: %1010 (not 0b1010). Underscores for readability: 25_000_000
- 4-digit hex
$0000 = uword. No type suffixes (no 0L). Cast: expr as type
- Augmented assignment:
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
; starts a comment to end of line — NOT a statement separator. One statement per line
- No
elif: use nested else { if ... }
- Type casting:
expression as type (e.g., bytevar as word). as has 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::Value syntax (double colon), declared inside a block. They are syntactic sugar for a list of const declarations, not a type. Use enums for related values, const for 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)