Skip to main content

Control Flow

R65 provides structured control flow that maps efficiently to 65816 branch and jump instructions. All constructs compile with zero overhead compared to hand-written assembly.


If Statements

Basic If

The condition must evaluate to bool or a comparable expression. The body executes only when the condition is true.

if x > 10 {
process();
}

if (flags & 0x80) != 0 {
handle_error();
}

if ready {
start_game();
}

If-Else

if health == 0 {
game_over();
} else {
continue_game();
}

If-Else Chain

Any number of else if clauses may appear. The final else is optional for statements.

if x < 10 {
category = 0;
} else if x < 20 {
category = 1;
} else if x < 30 {
category = 2;
} else {
category = 3;
}

If-Else as Expression

When used as an expression, both branches are required and must produce the same type. The last expression in each branch (without a trailing semicolon) is the branch's value.

let category: u8 = if x < 10 {
0
} else if x < 20 {
1
} else {
2
};

let abs_val: u8 = if x >= 0 { x } else { 0 - x };

else if chains are permitted in expression position.


Block Expressions

A block { ... } can be used as an expression. The last item in the block, written without a trailing semicolon, is the block's value. Variables declared inside the block are scoped to the block.

let result: u8 = {
let temp: u8 = compute();
temp + 1
};

let offset: u16 = {
let row: u16 = (y as u16) << 5;
row + (x as u16)
};

Loops

Infinite Loop: loop

Repeats indefinitely. Use break to exit or return to exit the enclosing function.

#[entry]
fn main() -> ! {
init();
loop {
wait_vblank();
update_game();
render();
}
}

// Polling loop
loop {
if HVBJOY & 0x01 != 0 {
break;
}
}

While Loop

The condition is checked before each iteration. If initially false, the body never executes.

while count > 0 {
process();
count -= 1;
}

while !ready {
wait();
}

let mut index = 0;
while index < 10 {
buffer[index] = 0;
index += 1;
}

For Loop (Range-Based)

Iterates over a range of integers. Only range syntax is supported -- there are no iterator-based for loops.

  • start..end -- exclusive: iterates from start to end - 1
  • start..=end -- inclusive: iterates from start to end

The loop variable is automatically declared as mutable with the type inferred from the range bounds.

// Exclusive range
for i in 0..256 {
buffer[i] = 0;
}

// Inclusive range
for i in 0..=255 {
table[i] = i as u8;
}

// Nested loops
for y in 0..8 {
for x in 0..8 {
process_tile(x, y);
}
}

// Using constants
const WIDTH: u8 = 32;
const HEIGHT: u8 = 28;
for row in 0..HEIGHT {
for col in 0..WIDTH {
draw_cell(col, row);
}
}

Labeled Loops

Any loop can have a label, enabling break and continue to target a specific enclosing loop.

Labels start with ' followed by an identifier and :. They are only valid on loop statements. Referencing a non-existent or non-enclosing label is a compile error.

'outer: for y in 0..8 {
for x in 0..8 {
if tile_map[y * 8 + x] == target {
break 'outer; // exit both loops
}
}
}

'rows: for y in 0..HEIGHT {
for x in 0..WIDTH {
if skip_row[y] {
continue 'rows; // skip to next row
}
process_cell(x, y);
}
}

Loop Expressions

A loop can be used as an expression when break carries a value. All break statements within the loop must provide a value of the same type.

let found_index: u8 = loop {
if buffer[i] == target {
break i;
}
i += 1;
if i >= len {
break 0xFF; // sentinel for "not found"
}
};

Break

break immediately exits a loop. break 'label exits the loop with the specified label. Using break outside any loop is a compile error.

break;                          // exit innermost loop
break 'label; // exit labeled loop
break value; // exit loop expression with value
break 'label value; // exit labeled loop expression with value
// Search with early exit
let mut index = 0;
let mut found = false;
while index < 256 {
if buffer[index] == target {
found = true;
break;
}
index += 1;
}

// Read until done
loop {
let input = read_controller();
if input == 0 {
break;
}
process(input);
}

Continue

continue skips the rest of the current iteration. For while and for, this re-checks the condition (and for for, increments the loop variable first). For loop, this jumps to the top of the loop body.

continue 'label targets the labeled loop. Using continue outside any loop is a compile error.

continue;                       // skip to next iteration
continue 'label; // skip to next iteration of labeled loop
let mut i = 0;
while i < 100 {
i += 1;

if (i & 0x01) != 0 { // skip odd numbers
continue;
}

process_even(i);
}

loop {
let status = read_status();

if status == 0xFF {
continue; // ignore invalid status
}

handle(status);

if done {
break;
}
}

// Labeled continue
'rows: for y in 0..HEIGHT {
for x in 0..WIDTH {
if skip_row[y] {
continue 'rows; // skip rest of this row
}
process_cell(x, y);
}
}

Nested Loop Patterns

Without labels, break only exits the innermost loop. A flag variable can propagate the exit outward:

let mut found = false;
let mut y = 0;
while y < 8 {
let mut x = 0;
while x < 8 {
if tile[y][x] == target {
found = true;
break; // breaks inner loop only
}
x += 1;
}
if found {
break; // breaks outer loop
}
y += 1;
}

Labeled break is cleaner and more efficient:

'outer: for y in 0..8 {
for x in 0..8 {
if tile[y][x] == target {
break 'outer; // exits both loops directly
}
}
}

Return

return immediately exits the current function. See Functions -- Return Values for register assignment conventions.

return;                         // exit function (implicit A return if typed)
return value; // return single value
return a, b; // return multiple values (no parentheses)
return a, b, c; // return three values

All return paths in a function must have identical return signatures.

Implicit A Return

If a function has a return type and the body ends without an explicit return, the current value of A is returned:

fn get_status() -> u8 {
A = STATUS & 0x0F;
// implicitly returns A
}

Early Return

return can appear anywhere in the function body to exit early:

fn validate(input @ A: u8) -> u8 {
if input == 0 {
return 0xFF; // early exit
}

if input > 100 {
return 100; // early exit
}

return input; // normal return
}

Multiple Return Values

Functions can return up to three values using registers:

fn get_xy() -> (u8, u8) {
X = PLAYER_X;
Y = PLAYER_Y;
return X, Y;
}

let (px, py) = get_xy();

Never Type: -> !

Functions that never return use the ! type. The compiler omits RTS/RTL. Common for entry points and error handlers.

#[entry]
fn main() -> ! {
init();
loop {
update();
}
}

fn fatal_error() -> ! {
SCREEN = 0x00; // black screen
loop {
asm!("STP"); // stop processor
}
}

A function declared -> ! that can actually return is a compile error.


Short-Circuit Evaluation

The logical operators && and || use short-circuit (lazy) evaluation.

Logical AND (&&): the right operand is evaluated only if the left operand is true.

if check_a() && check_b() {
execute();
}
// check_b() only called if check_a() returns true

Logical OR (||): the right operand is evaluated only if the left operand is false.

if quick_check() || slow_check() {
execute();
}
// slow_check() only called if quick_check() returns false

Multiple conditions can be chained:

if a && b && c {
execute();
}

if has_powerup || health > 50 || is_invincible {
allow_action();
}

Constant Folding

Compile-time constant conditions are evaluated during compilation and the unreachable branch is removed. Unlike C preprocessor #ifdef, the code is still type-checked before removal.

const DEBUG: bool = false;

if DEBUG {
log_message(); // entire block removed when DEBUG = false
}

if true {
always_runs(); // condition removed, body always executes
}

Examples

Game State Machine

A typical game loop dispatches on the current state each frame:

enum GameState { Menu, Playing, Paused, GameOver }

#[zeropage]
static mut STATE: GameState = GameState::Menu;

fn main() -> ! {
loop {
if STATE == GameState::Menu {
update_menu();
} else if STATE == GameState::Playing {
update_game();
} else if STATE == GameState::Paused {
update_pause();
} else {
update_game_over();
}

render();
wait_vblank();
}
}

Polling Loop with Timeout

Hardware polling with a timeout counter to avoid infinite hangs:

fn wait_ready(timeout @ X: u16) -> bool {
loop {
if (STATUS & READY_BIT) != 0 {
return true;
}

if timeout == 0 {
return false;
}

timeout -= 1;
wait_frame();
}
}

Memory Copy

A byte-by-byte memory copy using register aliases for zero overhead:

fn copy_memory(src: *u8, dst: *u8, count @ X: u16) {
if count == 0 {
return;
}

let mut index @ Y = 0;
loop {
dst[index] = src[index];
index += 1;
count -= 1;

if count == 0 {
break;
}
}
}

A binary search over a ROM lookup table with early return:

fn binary_search(target @ A: u8) -> u8 {
let mut low: u8 = 0;
let mut high: u8 = 255;

while low <= high {
let mid = low + ((high - low) >> 1);
let value = table[mid];

if value == target {
return mid;
} else if value < target {
low = mid + 1;
} else {
high = mid - 1;
}
}

return 0xFF; // not found
}