Statement Types
Leo supports several types of statements for controlling program flow and managing state.
Variable Declarations
Let Bindings
Declare immutable variables with let:
let balance: u64 = 1000u64;
let recipient: address = self.caller;
let is_valid: bool = true;
Type annotations are optional when the type can be inferred:
let amount = 100u64; // Type inferred as u64
let sum = amount + 50u64; // Type inferred as u64
Constant Declarations
Declare compile-time constants with const:
const MAX_SUPPLY: u64 = 1000000u64;
const DECIMALS: u8 = 6u8;
const ZERO_ADDRESS: address = aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc;
Constants must be initialized with literal values or constant expressions. They cannot depend on runtime values.
Assignments
Simple Assignment
Reassign variables (for mutable contexts):
let r1c1: u8 = board.r1.c1;
if row == 1u8 && col == 1u8 && r1c1 == 0u8 {
r1c1 = player;
}
Destructuring Assignment
Unpack tuples and structs:
// Tuple destructuring
let (remaining, transferred) = transfer_private(sender, receiver, amount);
// Struct member access
let r1c1: u8 = board.r1.c1;
let r1c2: u8 = board.r1.c2;
let r1c3: u8 = board.r1.c3;
Conditional Statements
If Statements
Execute code based on conditions:
if player == 1u8 || player == 2u8 {
// Valid player
proceed();
}
If-Else Statements
Provide alternative execution paths:
if check_for_win(updated, 1u8) {
return (updated, 1u8);
} else if check_for_win(updated, 2u8) {
return (updated, 2u8);
} else {
return (updated, 0u8);
}
Nested Conditions
Combine multiple conditions:
if row == 1u8 && col == 1u8 && r1c1 == 0u8 {
r1c1 = player;
} else if row == 1u8 && col == 2u8 && r1c2 == 0u8 {
r1c2 = player;
} else if row == 1u8 && col == 3u8 && r1c3 == 0u8 {
r1c3 = player;
} else if row == 2u8 && col == 1u8 && r2c1 == 0u8 {
r2c1 = player;
}
Loops
For Loops
Iterate over a range of values:
// Exclusive range: 0 to 9
for i: u32 in 0u32..10u32 {
sum = sum + i;
}
// Inclusive range: 0 to 10
for i: u32 in 0u32..=10u32 {
total = total + i;
}
Loop bounds must be known at compile time. Leo unrolls all loops during compilation.
Loop Examples
// Initialize array elements
let mut arr: [u32; 5] = [0u32; 5];
for i: u32 in 0u32..5u32 {
arr[i] = i * 2u32;
}
// Sum array elements
let numbers: [u32; 10] = [1u32, 2u32, 3u32, 4u32, 5u32, 6u32, 7u32, 8u32, 9u32, 10u32];
let mut sum: u32 = 0u32;
for i: u32 in 0u32..10u32 {
sum = sum + numbers[i];
}
Assertions
Assert Statement
Verify conditions and fail if false:
assert(player == 1u8 || player == 2u8);
assert(1u8 <= row && row <= 3u8);
assert(1u8 <= col && col <= 3u8);
Assertions in finalize functions:
final fn finalize_play() {
// Check lottery expiration
assert(block.height <= 1000u32);
// Check random condition
assert(ChaCha::rand_bool());
// Check winner limit
let winners: u8 = num_winners.get_or_use(0u8, 0u8);
assert(winners < 5u8);
}
If an assertion fails, the entire transaction is reverted. Use assertions for critical invariants and validation.
Return Statements
Simple Returns
Return a single value:
fn get_max_supply() -> u64 {
return MAX_SUPPLY;
}
fn new() -> Board {
return Board {
r1: Row { c1: 0u8, c2: 0u8, c3: 0u8 },
r2: Row { c1: 0u8, c2: 0u8, c3: 0u8 },
r3: Row { c1: 0u8, c2: 0u8, c3: 0u8 },
};
}
Multiple Returns
Return tuples for multiple values:
fn transfer_private(sender: token, receiver: address, amount: u64)
-> (token, token) {
let difference: u64 = sender.amount - amount;
let remaining: token = token {
owner: sender.owner,
amount: difference,
};
let transferred: token = token {
owner: receiver,
amount: amount,
};
return (remaining, transferred);
}
Early Returns
Return early from functions:
fn check_for_win(b: Board, p: u8) -> bool {
return
(b.r1.c1 == p && b.r1.c2 == p && b.r1.c3 == p) || // row 1
(b.r2.c1 == p && b.r2.c2 == p && b.r2.c3 == p) || // row 2
(b.r3.c1 == p && b.r3.c3 == p && b.r3.c3 == p) || // row 3
(b.r1.c1 == p && b.r2.c1 == p && b.r3.c1 == p) || // column 1
(b.r1.c2 == p && b.r2.c3 == p && b.r3.c2 == p) || // column 2
(b.r1.c3 == p && b.r2.c3 == p && b.r3.c3 == p) || // column 3
(b.r1.c1 == p && b.r2.c2 == p && b.r3.c3 == p) || // diagonal
(b.r1.c3 == p && b.r2.c2 == p && b.r3.c1 == p); // other diagonal
}
Block Statements
Scope Blocks
Create new scopes with curly braces:
{
let temp: u64 = calculate_value();
process(temp);
// temp goes out of scope here
}
Function Bodies
Function bodies are block statements:
fn mint_private(receiver: address, amount: u64) -> token {
return token {
owner: receiver,
amount: amount,
};
}
Expression Statements
Expressions can be used as statements:
// Function call as statement
validate_player(player);
// Method call as statement
token_balance.add(amount);
Statement Composition
Complex Control Flow
Combine statements for complex logic:
fn make_move(player: u8, row: u8, col: u8, board: Board) -> (Board, u8) {
// Assertions
assert(player == 1u8 || player == 2u8);
assert(1u8 <= row && row <= 3u8);
assert(1u8 <= col && col <= 3u8);
// Variable declarations
let r1c1: u8 = board.r1.c1;
let r1c2: u8 = board.r1.c2;
let r1c3: u8 = board.r1.c3;
// ... more variables
// Conditional updates
if row == 1u8 && col == 1u8 && r1c1 == 0u8 {
r1c1 = player;
} else if row == 1u8 && col == 2u8 && r1c2 == 0u8 {
r1c2 = player;
}
// ... more conditions
// Create updated board
let updated: Board = Board {
r1: Row { c1: r1c1, c2: r1c2, c3: r1c3 },
r2: Row { c1: r2c1, c2: r2c2, c3: r2c3 },
r3: Row { c1: r3c1, c2: r3c2, c3: r3c3 },
};
// Check win conditions and return
if check_for_win(updated, 1u8) {
return (updated, 1u8);
} else if check_for_win(updated, 2u8) {
return (updated, 2u8);
} else {
return (updated, 0u8);
}
}
Best Practices
1. Initialize Variables
// Good: Clear initialization
let balance: u64 = 0u64;
let is_valid: bool = false;
// Avoid: Uninitialized variables
let balance: u64; // Compilation error
2. Use Descriptive Names
// Good: Clear intent
let sender_balance: u64 = sender.amount;
let receiver_balance: u64 = receiver.amount;
// Avoid: Unclear names
let x: u64 = sender.amount;
let y: u64 = receiver.amount;
3. Limit Nesting Depth
// Good: Early returns reduce nesting
fn validate(value: u32) -> bool {
if value < MIN {
return false;
}
if value > MAX {
return false;
}
return true;
}
// Avoid: Deep nesting
fn validate(value: u32) -> bool {
if value >= MIN {
if value <= MAX {
return true;
} else {
return false;
}
} else {
return false;
}
}
4. Assert Preconditions Early
// Good: Validate inputs first
fn transfer(sender: token, amount: u64) -> token {
assert(sender.amount >= amount);
return token {
owner: sender.owner,
amount: sender.amount - amount,
};
}
// Good: Logical grouping
fn process() {
// Validation
assert(is_valid());
assert(amount > 0u64);
// Calculation
let fee: u64 = calculate_fee(amount);
let net: u64 = amount - fee;
// State updates
update_balance(net);
record_transaction(fee);
}
Common Patterns
Guard Clauses
fn process_payment(amount: u64, sender: token) -> token {
// Guard against invalid inputs
if amount == 0u64 {
return sender;
}
if sender.amount < amount {
return sender;
}
// Main logic
return token {
owner: sender.owner,
amount: sender.amount - amount,
};
}
State Machines
const STATE_PENDING: u8 = 0u8;
const STATE_ACTIVE: u8 = 1u8;
const STATE_COMPLETE: u8 = 2u8;
fn transition_state(current: u8, event: u8) -> u8 {
if current == STATE_PENDING && event == EVENT_START {
return STATE_ACTIVE;
} else if current == STATE_ACTIVE && event == EVENT_FINISH {
return STATE_COMPLETE;
} else {
return current;
}
}
Accumulation
fn sum_array(arr: [u32; 10]) -> u32 {
let mut total: u32 = 0u32;
for i: u32 in 0u32..10u32 {
total = total + arr[i];
}
return total;
}
Next Steps
Functions Build complete functions
Operators Use operators in statements
Data Types Work with different types
Finalize On-chain state changes