Statements
This page specifies R65's statements and control flow constructs: variable declarations, assignments, conditionals, loops, match expressions, and related forms.
Variable Declarations
let Bindings
The let statement declares a local variable. Every variable must have an explicit type annotation or a register alias from which the type can be inferred.
Syntax:
let name: Type = initializer;
let mut name: Type = initializer;
let mut name: Type;
Semantics:
letdeclares an immutable binding. The variable cannot be reassigned after initialization.let mutdeclares a mutable binding.- An initializer is required for immutable bindings. Mutable bindings may omit the initializer, in which case the variable holds an undefined value until assigned.
- The type annotation is required unless a register alias is present (see below).
Examples:
let x: u8 = 10;
let mut counter: u16 = 0;
let mut temp: u8; // uninitialized, must assign before use
Register Alias Bindings
A let binding with @ Register aliases the variable to a hardware register. The variable name becomes a zero-cost alias -- all reads and writes go directly to the register.
Syntax:
let name @ Register = initializer;
let name @ Register: Type = initializer;
Semantics:
- When a register alias is present, the type is inferred from the register's current type in the function's processor mode. An explicit type annotation is optional but must match if provided.
- In default mode (m8),
@ Ainfersu8. In m16 mode (when the function has a@ A: u16parameter),@ Ainfersu16. @ Xand@ Yalways inferu16(X/Y are always 16-bit in R65).
Examples:
let value @ A = 10; // value aliases A, inferred u8 in m8 mode
let index @ X = 0; // index aliases X, inferred u16
let hitpoints @ A = PLAYER.health; // A holds hitpoints
Register aliases have zero runtime cost -- no memory allocation occurs. The variable name is a compile-time alias for the register.
Tuple Destructuring
Multiple return values from a function call can be destructured into separate bindings.
Syntax:
let (a, b) = function_call();
Semantics:
- The right-hand side must be a function call returning a tuple type.
- Each name in the tuple pattern binds to the corresponding return value.
- The number of names must match the number of return values.
Examples:
fn get_position() -> (u8, u8) {
return X, Y;
}
let (px, py) = get_position();
Assignments
Simple Assignment
Syntax:
target = expression;
Semantics:
- The target must be a mutable variable, hardware register, array element, struct field, or dereferenced pointer.
- The expression type must be compatible with the target type.
Examples:
x = 10;
A = value;
buffer[i] = 0;
player.health = 100;
*ptr = 5;
Compound Assignment
Compound assignment operators combine a binary operation with assignment.
Syntax:
target += expression;
target -= expression;
target &= expression;
target |= expression;
target ^= expression;
target <<= constant;
target >>= constant;
target *= constant; // constant must be 1, 2, 4, or 8
target /= constant; // constant must be 1, 2, 4, or 8
Semantics:
target op= exprdesugars totarget = target op expr.- The same restrictions from the base operator apply. Shift amounts must be compile-time constants. Multiply/divide constants must be 1, 2, 4, or 8.
Examples:
counter += 1;
flags &= 0x0F;
value <<= 2;
Increment and Decrement
Postfix ++ and -- operators increment or decrement a value by one.
Syntax:
target++;
target--;
Semantics:
- Statement-only: these operators do not produce a value and cannot be used inside expressions.
- Postfix form only. There is no prefix
++xor--x. - Desugars to
target += 1/target -= 1in the parser. - Works with variables, hardware registers, array elements, and struct fields.
Examples:
counter++;
X--;
buffer[i]++;
player.health--;
Multiple Assignment
Multiple assignment destructures a multi-value return into existing variables.
Syntax:
a, b = function_call();
Semantics:
- The right-hand side must be a function call returning multiple values.
- Each target must be an assignable location (mutable variable, register, array element, or struct field).
If Statements
Basic If
Syntax:
if condition {
// body
}
Semantics:
- The condition must evaluate to
boolor a comparable expression. - The body executes only when the condition is true.
Examples:
if x > 10 {
process();
}
if (flags & 0x80) != 0 {
handle_error();
}
If-Else
Syntax:
if condition {
// true branch
} else {
// false branch
}
Examples:
if health == 0 {
game_over();
} else {
continue_game();
}
If-Else If-Else Chain
Syntax:
if condition1 {
// branch 1
} else if condition2 {
// branch 2
} else {
// default
}
Any number of else if clauses may appear. The final else is optional for statements (but required for if-as-expression; see below).
Examples:
if x < 10 {
category = 0;
} else if x < 20 {
category = 1;
} else {
category = 2;
}
If-Else as Expression
When if-else appears in expression position (e.g., on the right side of a let binding), it produces a value.
Syntax:
let result: Type = if condition {
expr_true
} else {
expr_false
};
Semantics:
- Both branches are required. An
ifwithoutelsecannot be used as an expression. - Both branches must produce the same type.
- The last expression in each branch (without a trailing semicolon) is the branch's value.
else ifchains are permitted.
Examples:
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 };
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.
Syntax:
{
statement;
statement;
expression // no semicolon -- this is the block's value
}
Semantics:
- All statements inside the block execute in order.
- The final expression (without semicolon) determines the block's type and value.
- Variables declared inside the block are scoped to the block.
Examples:
let result: u8 = {
let temp: u8 = compute();
temp + 1
};
let offset: u16 = {
let row: u16 = (y as u16) << 5;
row + (x as u16)
};
Block expressions are useful for complex initializations that require intermediate variables without polluting the enclosing scope.
Loops
Infinite Loop: loop
Syntax:
loop {
// body
}
Semantics:
- Repeats the body indefinitely.
- Must use
breakto exit orreturnto exit the enclosing function. - The primary pattern for main game loops and event loops.
Examples:
#[entry]
fn main() -> ! {
init();
loop {
wait_vblank();
update_game();
render();
}
}
// Polling loop
loop {
if HVBJOY & 0x01 != 0 {
break;
}
}
While Loop
Syntax:
while condition {
// body
}
Semantics:
- The condition is checked before each iteration.
- If the condition is initially false, the body never executes.
- The loop exits when the condition becomes false.
Examples:
while count > 0 {
process();
count -= 1;
}
while !ready {
wait();
}
For Loop (Range-Based)
Syntax:
for variable in start..end {
// body (exclusive: iterates start to end-1)
}
for variable in start..=end {
// body (inclusive: iterates start to end)
}
Semantics:
start..enditerates fromstart(inclusive) toend(exclusive).start..=enditerates fromstart(inclusive) toend(inclusive).- The loop variable is automatically declared as mutable with the type inferred from the range bounds.
- Range bounds must be integer expressions.
- Only range-based iteration is supported. There are no iterator-based
forloops.
The exclusive form desugars to:
let mut i = start;
while i < end {
// body
i = i + 1;
}
Examples:
// Clear a buffer
for i in 0..256 {
buffer[i] = 0;
}
// Nested loops
for y in 0..8 {
for x in 0..8 {
process_tile(x, y);
}
}
// Inclusive range
for i in 0..=255 {
table[i] = i as u8;
}
// Using constants
const WIDTH: u8 = 32;
for col in 0..WIDTH {
draw_cell(col);
}
Labeled Loops
Any loop (loop, while, for) can have a label. Labels enable break and continue to target a specific enclosing loop in nested loop constructs.
Syntax:
'label: loop { }
'label: while condition { }
'label: for i in start..end { }
Rules:
- Labels start with
'followed by an identifier and:. - Labels are only valid on loop statements.
break 'labelandcontinue 'labelmust reference an enclosing labeled loop.- Referencing a non-existent or non-enclosing label is a compile error.
Examples:
'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.
Syntax:
let result: Type = loop {
// ...
break value;
};
Semantics:
- The
breakstatement supplies the value of the loop expression. - All
breakstatements within the loop must provide a value of the same type. - A
loopexpression without a value-carryingbreakhas type!(never).
Examples:
let found_index: u8 = loop {
if buffer[i] == target {
break i;
}
i += 1;
if i >= len {
break 0xFF; // sentinel for "not found"
}
};
Break
Syntax:
break;
break 'label;
break value; // only inside loop expressions
break 'label value; // only inside labeled loop expressions
Semantics:
break;exits the innermost enclosing loop.break 'label;exits the loop with the specified label.break value;exits the loop and provides the value of the loop expression.- Using
breakoutside any loop is a compile error. - Using
break 'labelwith a label that does not refer to an enclosing loop is a compile error.
Continue
Syntax:
continue;
continue 'label;
Semantics:
continue;skips the rest of the current iteration and jumps to the next iteration of the innermost enclosing loop.- For
whileandfor, this means re-checking the condition (and forfor, incrementing the loop variable first). - For
loop, this jumps to the top of the loop body.
- For
continue 'label;targets the labeled loop.- Using
continueoutside any loop is a compile error.
Return
Syntax:
return;
return value;
return a, b;
return a, b, c;
Semantics:
return;exits the current function. If the function declares a return type, the value currently in the A register is returned implicitly.return value;returns a single value (placed in A by default, or as specified by the function's return convention).return a, b;andreturn a, b, c;return multiple values. No parentheses are used. Return registers are assigned based on the function's return type (see Functions).- All return paths in a function must have identical return signatures.
returnimmediately exits the function at any point in the body.
Implicit A Return
If a function has a return type and the body ends without an explicit return, the current value of the A register is returned.
fn get_status() -> u8 {
A = STATUS;
// implicitly returns A
}
Never-Returning Functions
Functions annotated with -> ! never return to their caller.
#[entry]
fn main() -> ! {
init();
loop {
update();
}
// no return needed; -> ! means "never returns"
}
Early Return
return can appear anywhere in the function body to exit early:
fn validate(input @ A: u8) -> u8 {
if input == 0 {
return 0xFF;
}
return input;
}
Match Expressions
Basic Match
The match expression tests a scrutinee against a sequence of patterns and executes the first matching arm. See Match Expressions for the full reference.
Syntax:
match scrutinee {
pattern1 => expression1,
pattern2 => expression2,
_ => default_expression,
}
Semantics:
- The scrutinee is evaluated once, then each arm's pattern is tested in order.
- The first matching arm's expression is executed.
- All arms must produce the same type when
matchis used as an expression. - The trailing comma after the last arm is optional.
- The match must be exhaustive: every possible value of the scrutinee must be covered by at least one pattern, or a wildcard/identifier pattern must be present.
Supported scrutinee types: u8, i8, u16, i16, bool, enums.
Pattern Types
Literal Patterns
Match against integer or boolean constants.
let result: u8 = match tile_id {
0 => 10,
1 => 20,
2 => 30,
_ => 0,
};
Enum Patterns
Match against enum variants. When all variants are covered, no wildcard arm is needed.
enum Direction { North = 0, East, South, West }
let dx: i8 = match dir {
Direction::North => 0,
Direction::East => 1,
Direction::South => 0,
Direction::West => -1,
};
Range Patterns
Match against a contiguous range of integer values.
let category: u8 = match tile_id {
0..=15 => 1, // inclusive: matches 0, 1, ..., 15
16..32 => 2, // exclusive: matches 16, 17, ..., 31
32..=47 => 3,
_ => 0,
};
start..=endis an inclusive range (matches start through end).start..endis an exclusive range (matches start through end minus 1).- Both endpoints must be integer literals.
- Empty ranges are a compile error (
5..5,5..=3). - Range patterns only match integer scrutinee types (
u8,i8,u16,i16).
Or Patterns
Combine multiple patterns with |.
let result: u8 = match input {
0 | 1 | 2 => 10,
3 | 4 | 5 => 20,
_ => 0,
};
Range patterns can appear inside or-patterns:
let zone: u8 = match tile_id {
0..=3 | 10..=13 => 1,
4..=9 => 2,
_ => 0,
};
Wildcard Pattern
_ matches any value. It is typically used as the last arm to cover all remaining cases.
let result: u8 = match val {
0 => 100,
_ => 0,
};
Identifier Pattern
An identifier pattern binds the matched value to a new variable within the arm's expression.
let result: u8 = match val {
0 => 100,
other => other + 1, // 'other' holds the scrutinee's value
};
Exhaustiveness
The compiler enforces that match expressions cover all possible values:
bool: Bothtrueandfalsemust be covered, or a wildcard must be present.- Enums: All variants must be covered, or a wildcard must be present.
- Integer types (
u8,i8,u16,i16): A wildcard_or identifier pattern is required because enumerating all values is impractical.
// OK: all bool values covered
let x: u8 = match flag {
true => 1,
false => 0,
};
// ERROR: non-exhaustive match, missing 'false'
let x: u8 = match flag {
true => 1,
};
// OK: wildcard covers remaining integers
let x: u8 = match val {
0 => 10,
_ => 0,
};
Match as Expression
match is an expression and produces a value. It can appear in let bindings, return statements, assignments, or any other expression position.
let category: u8 = match tile_id {
0..=15 => 0,
16..=31 => 1,
_ => 2,
};
return match state {
GameState::Playing => 1,
_ => 0,
};
Match as Statement
When used as a statement (not in expression position), match arms can contain blocks with arbitrary statements. The arms do not need to produce a value.
match state {
GameState::Menu => {
update_menu();
},
GameState::Playing => {
update_game();
check_collisions();
},
_ => {},
}
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
Chained Conditions
Multiple conditions can be chained:
if a && b && c {
execute();
}
if has_powerup || health > 50 || is_invincible {
allow_action();
}
Expression Statements
Any expression followed by a semicolon is an expression statement. The expression is evaluated and its result is discarded.
process(); // function call
A + 1; // value discarded (unusual but legal)
Function calls are the most common expression statement.
Error Conditions
Break/Continue Outside Loop
Using break or continue outside any loop is a compile error:
fn invalid() {
break; // ERROR: break outside of loop
}
fn also_invalid() {
if true {
continue; // ERROR: continue outside of loop
}
}
Non-Exhaustive Match
A match expression that does not cover all possible values is a compile error:
let x: u8 = match val {
0 => 10,
// ERROR: non-exhaustive patterns; add a wildcard `_` arm
};
Mismatched Branch Types
When if-else or match is used as an expression, all branches must produce the same type:
let x: u8 = if flag {
10 // u8
} else {
1000 // u16 -- ERROR: type mismatch between branches
};