Skip to main content

Types

R65 provides a minimal type system designed around the 65816 processor's register sizes and addressing modes. All types have fixed, known sizes at compile time. There are no alignment requirements and no runtime bounds checking.

Primitive Types

Integer Types

TypeSizeRangeSignedness
u81 byte0 to 255Unsigned
i81 byte-128 to 127Signed
u162 bytes0 to 65535Unsigned
i162 bytes-32768 to 32767Signed

All integer arithmetic wraps on overflow. There is no runtime range checking.

let x: u8 = 255;
x = x + 1; // Wraps to 0

Boolean Type

TypeSizeRepresentation
bool1 byteStored as u8 (0 or 1)

bool is a distinct type from u8 but occupies the same storage. false is 0, true is 1.

let flag: bool = true;
let raw: u8 = flag as u8; // 1
let back: bool = raw as bool; // true (any non-zero value becomes true)

Hardware Register Types

Hardware registers are global variables whose types depend on the current processor mode. See the Registers section for full documentation.

Mode-Dependent Registers

The accumulator's type changes based on function mode:

Registerm8 Mode (default)m16 Mode (@ A: u16 param)
Au8u16
Bu8not available (compile error)

m8 mode is the default. A function enters m16 mode only when it declares a @ A: u16 parameter.

B is the hidden high byte of the 16-bit accumulator, accessed internally via the XBA instruction. It is only meaningful in m8 mode; using B in a function with @ A: u16 is a compile error.

Mode-Independent Registers

These registers have fixed types regardless of processor mode:

RegisterTypeNotes
Xu16Index register (always x16)
Yu16Index register (always x16)
STATUSu8Processor status (NVMXDIZC)
Du16Direct Page register
DBRu8Data Bank Register
PBRu8Program Bank Register (read-only)
Su16Stack Pointer

X and Y are always 16-bit in R65 (x16 mode is assumed). Attempting to bind a parameter with @ X: u8 or @ Y: u8 is a compile error.

fn example(val @ A: u8) {
// A: u8 (m8 mode)
// X: u16, Y: u16 (always)
X = 1000; // OK
A = 10; // OK
}

fn wide(val @ A: u16) {
// A: u16 (m16 mode)
// B: not available
A = 1000; // OK
// B = 5; // ERROR: B not available in m16 mode
}

Composite Types

Arrays

Fixed-size arrays with elements packed contiguously in memory.

[T; N]   // Array of N elements of type T
PropertyValue
SizeN * sizeof(T) bytes
LayoutPacked, no padding
Bounds checkingNone
Pass semanticsBy reference only (use pointers)
#[ram]
static mut BUFFER: [u8; 256] = [0; 256];

#[ram]
static mut MESSAGE: [u8; 16] = "Hello, World!"; // String literal, zero-padded

Arrays cannot be passed by value to functions. Pass a pointer instead.

Out-of-bounds access is undefined behavior. The compiler performs no runtime checks.

Structs

Structs are packed aggregates with fields laid out in declaration order. No padding is inserted between fields.

struct Player {
x: u8,
y: u8,
health: u16,
}
// Total size: 4 bytes (1 + 1 + 2)
PropertyValue
LayoutPacked, declaration order
SizeSum of all field sizes
AlignmentNone
MethodsNone (use free functions)
Pass semanticsBy reference only (use pointers)

Structs cannot be passed by value or directly assigned. Use pointers for indirection.

#[ram]
static mut PLAYER: Player;

PLAYER.x = 10;
PLAYER.health = 100;

let p = Player { x: 10, y: 20, health: 100 };

Enums

C-style enums with explicit or auto-incrementing discriminant values. No data-carrying variants.

enum Direction {
North = 0,
East, // 1
South, // 2
West, // 3
}
PropertyValue
RepresentationSmallest integer type that fits all discriminants
Sizeu8 if all values fit in 0..255, otherwise u16
Data variantsNot supported

Enums can be cast to their underlying integer type with as:

let dir = Direction::North;
let value: u8 = dir as u8; // 0

Pointers

Pointers are raw memory addresses with no metadata, bounds, or ownership tracking.

Near Pointers

let ptr: *u8 = 0x2000;    // 2 bytes, 16-bit address within current data bank

Near pointers address 64KB within the current Data Bank Register (DBR).

Far Pointers

let ptr: far *u8 = 0x01_2000;   // 3 bytes, 24-bit address (includes bank)

Far pointers address the full 16MB address space.

Pointer to Slice (Unsized Array)

let ptr: *[u8];   // Near pointer to unsized array

*[T] points to a contiguous sequence of T with unknown length. A pointer to a fixed-size array (*[T; N]) coerces implicitly to *[T], enabling generic array parameters:

fn process(data: *[u8]) { }

static TABLE: [u8; 256] = [0; 256];
process(&TABLE); // *[u8; 256] coerces to *[u8]

Pointer Sizes

Pointer TypeSizeRange
*T2 bytes64KB (current bank)
far *T3 bytes16MB (full address space)
*[T]2 bytesSame as *T
far *[T]3 bytesSame as far *T

Operations

Pointers support dereference, indexing, arithmetic, comparison, and casting:

*ptr = 42;                  // Dereference (write)
let v = *ptr; // Dereference (read)
ptr[Y] = 5; // Indexed access
ptr = ptr + 10; // Pointer arithmetic (scaled by sizeof(T))
if ptr as u16 != 0 { } // Null check (manual)

Struct field access through pointers is auto-dereferenced:

#[zeropage]
static mut PTR: *Player;
PTR.x = 10; // Equivalent to (*PTR).x = 10

Function Pointers

Function pointers store the address of a callable function.

TypeSizeCalling Convention
fn()2 bytesNear (JSR/RTS)
far fn()3 bytesFar (JSL/RTL)
fn add(a @ A: u8, b @ X: u16) -> u8 { return A; }

let callback: fn(u8, u16) -> u8 = add;

The calling convention (near vs. far) is encoded in the type. A fn() and far fn() are distinct, incompatible types.

Tuples

Tuples exist only as multiple return value types. They cannot be stored in variables, passed as parameters, or used in struct fields.

fn unpack(value: u16) -> (u8, u8) {
A = value as u8;
B = (value >> 8) as u8;
return A, B;
}

let (low, high) = unpack(0x1234);

Supported tuple forms and their register mappings:

Return TypeRegisters UsedMode Requirement
(u8, u8)A, Bm8
(u8, u16)A, Xm8
(u16, u16)A, Xm16

Tuples are destructured at the call site only. There is no general-purpose tuple type.

Never Type

The never type ! indicates a function that never returns. The compiler omits RTS/RTL generation for such functions.

fn main_loop() -> ! {
loop {
wait_for_vblank();
update_game();
render();
}
}

Use -> ! for the main game loop or any function that intentionally loops forever or halts the processor.

Type Aliases

Type aliases create alternate names for existing types. They are fully transparent to the type checker.

type Word = u16;
type Byte = u8;
type Callback = fn(u8) -> u8;

let w: Word = 0x1234; // Same as: let w: u16 = 0x1234;

Type Sizes Summary

TypeSize
u8, i8, bool1 byte
u16, i162 bytes
*T, *[T]2 bytes
far *T, far *[T]3 bytes
fn()2 bytes
far fn()3 bytes
[T; N]N * sizeof(T)
structSum of field sizes
enumu8 or u16 (inferred)

Type Conversions

All type conversions require the explicit as keyword. There are no implicit conversions between types, with the exception of integer promotion in binary expressions (see below).

Cast Behaviors

CastBehaviorTypical Cost
u8 as u16Zero-extend2-4 cycles
i8 as i16Sign-extend4-8 cycles
u16 as u8Truncate (low byte)0-2 cycles
u8 as i8Reinterpret bits0 cycles
i8 as u8Reinterpret bits0 cycles
bool as u8false = 0, true = 10 cycles
u8 as bool0 = false, non-zero = true0 cycles
*T as *UPointer reinterpret0 cycles
*T as u16Pointer to integer0 cycles
u16 as *TInteger to pointer0 cycles
far *T as u16Truncate (drop bank)0 cycles
Enum as u8/u16Enum to underlying integer0 cycles
let small: u8 = 42;
let wide: u16 = small as u16; // Zero-extend: 0x002A
let back: u8 = wide as u8; // Truncate: 0x2A

let signed: i8 = -5;
let wide_s: i16 = signed as i16; // Sign-extend: 0xFFFB

Type Checking Rules

Integer Assignment

Integer types are compatible with each other for assignment. Explicit casts are recommended but not strictly required for integer-to-integer assignment:

let a: u8 = 10;
let b: u16 = 20;

b = a; // OK: integer types are compatible
a = b; // OK: truncates (programmer's responsibility)
b = a as u16; // Preferred: explicit cast makes intent clear

Non-integer types (pointers, structs, enums) require exact type matches.

Implicit Integer Promotion

When a binary operator has mixed-size integer operands, the smaller operand is automatically widened to match the larger:

let a: u8 = 10;
let b: u16 = 1000;
let c: u16 = a + b; // a is implicitly promoted to u16

This applies to arithmetic (+, -), bitwise (&, |, ^), and comparison (==, <, etc.) operators.

Pointer Type Checking

Pointer types must match exactly. Near and far pointers are distinct types, as are pointers to different element types:

let p1: *u8 = 0x2000;
let p2: *u16 = 0x3000;
let p3: far *u8 = 0x01_2000;

// p1 = p2; // ERROR: *u8 vs *u16
// p1 = p3; // ERROR: *u8 vs far *u8

p1 = p2 as *u8; // OK: explicit cast
p1 = p3 as *u8; // OK: explicit cast (drops bank byte)

Function Call Type Checking

Argument types must match parameter types exactly. No implicit conversions are performed at call boundaries:

fn add(a: u8, b: u8) -> u8 { return a + b; }

let x: u8 = 10;
let y: u16 = 20;

add(x, x); // OK
// add(x, y); // ERROR: u16 does not match u8
add(x, y as u8); // OK: explicit cast

Register Alias Type Checking

When binding a variable to a hardware register, the declared type must match the register's current type (determined by the function's mode):

fn example() {
let value @ A: u8 = 10; // OK: A is u8 in m8 mode
// let value @ A: u16 = 10; // ERROR: A is u8 in m8 mode
}

fn wide(input @ A: u16) {
let value @ A: u16 = 1000; // OK: A is u16 in m16 mode
}

Pointer Auto-Dereference

Struct field access through pointers is auto-dereferenced. No explicit (*ptr).field syntax is required:

struct Player { x: u8, y: u8, health: u16 }

#[zeropage]
static mut PTR: *Player;

PTR.x = 10; // Auto-dereferences: (*PTR).x = 10
let hp = PTR.health; // Auto-dereferences: (*PTR).health

Type Inference

R65 has very limited type inference.

What Is Inferred

Numeric literal types are inferred from the context in which they appear:

let x: u8 = 10;     // 10 inferred as u8
let y: u16 = 1000; // 1000 inferred as u16

Register alias types are inferred from the register's current type:

fn example() {
let value @ A = 10; // Inferred as u8 (A is u8 in m8 mode)
}

fn wide(input @ A: u16) {
let value @ A = 1000; // Inferred as u16 (A is u16 in m16 mode)
}

What Is Not Inferred

  • Variable types without a register binding or explicit annotation
  • Function return types
  • Function parameter types
// let z = 10;   // ERROR: cannot infer type without annotation
let z: u8 = 10; // OK: explicit type