Skip to main content

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