Skip to main content
Learn how to write well-structured Leo programs following best practices and conventions used in the Leo compiler codebase.

Project Structure

A Leo project follows a standardized directory structure:
my_project/
├── program.json          # Project manifest
├── src/
│   └── main.leo         # Main program file
├── tests/               # Test files
│   └── test_*.leo
├── build/               # Compiled output
│   ├── main.aleo
│   └── imports/
├── outputs/             # Compiler artifacts
└── .gitignore

Creating a New Project

Use the Leo CLI to initialize a new project:
leo new my_program
cd my_program
This creates a project structure with a basic program template:
src/main.leo
program my_program.aleo {
    @noupgrade
    constructor() {}

    fn main(public a: u32, b: u32) -> u32 {
        let c: u32 = a + b;
        return c;
    }
}
The @noupgrade annotation prevents program upgrades after deployment. Other options include @admin(address="...") and @checksum(mapping="...", key="...") for controlled upgrades.

Program Manifest (program.json)

The program.json file defines your project metadata and dependencies:
program.json
{
  "program": "my_program.aleo",
  "version": "0.1.0",
  "description": "",
  "license": "MIT",
  "dependencies": null,
  "dev_dependencies": null
}

Key Fields

  • program: Must match your program name and end with .aleo
  • version: Semantic version of your program
  • dependencies: External programs your code imports
  • dev_dependencies: Dependencies only needed for tests
The Leo version is automatically tracked and doesn’t need manual specification.

Code Organization

Function Definitions

Leo supports multiple function types:
1

Regular Functions

Standard functions that execute on-chain:
fn add(a: u32, b: u32) -> u32 {
    return a + b;
}
2

Async Functions with Finalize

Functions that return Final for on-chain finalization:
mapping Yo: u32 => u32;

fn main() -> Final {
    let f: Final = final { finalize_main(1u32, 1u32); };
    return f;
}
3

Final Functions

Finalize functions that run in on-chain execution:
final fn finalize_main(a: u32, b: u32) {
    Mapping::set(Yo, a, b);
}

Data Structures

Structs

Define custom data types:
struct Foo {
    x: u32,
    y: u32
}

fn create_foo() -> Foo {
    return Foo { x: 0, y: 0 };
}

Records

Private data structures with ownership:
record Token {
    owner: address,
    amount: u64,
}

fn mint_token() -> Token {
    return Token {
        owner: self.signer,
        amount: 100u64,
    };
}
Records must have an owner field of type address. They represent private, owned assets.

Mappings

On-chain key-value storage:
mapping users: address => bool;

fn register() -> Final {
    let caller = self.caller;
    return final { finalize_register(caller); };
}
final fn finalize_register(addr: address) {
    Mapping::set(users, addr, true);
}

Arrays and Tuples

Fixed-size Arrays

fn array_example() -> [u8; 3] {
    let arr: [u8; 3] = [1u8, 2u8, 3u8];
    return arr;
}

Nested Arrays

fn nested(a: [[[bool; 2]; 2]; 2]) -> [[[bool; 2]; 2]; 2] {
    a[0u8][0u8][0u8] = true;
    a[1u8][1u8] = [true, false];
    return a;
}

Tuples

fn tuple_example() -> (u32, bool, address) {
    return (42u32, true, self.signer);
}

Loops

Leo supports range-based for loops:
fn sort_array(xs: [u8; 8]) -> [u8; 8] {
    for i: u8 in 0u8..7u8 {
        for j: u8 in 0u8..7u8 - i {
            if xs[j] < xs[j+1u8] {
                let tmp: u8 = xs[j];
                xs[j] = xs[j+1u8];
                xs[j+1u8] = tmp;
            }
        }
    }
    return xs;
}
Loops are unrolled at compile time. Keep iteration counts reasonable to avoid excessive code generation.

Naming Conventions

Program Names

Program names must:
  • End with .aleo
  • Start with a letter (not underscore or number)
  • Contain only ASCII alphanumeric characters and underscores
  • Not contain the keyword aleo in the name itself
  • Not be a SnarkVM reserved keyword
Invalid names:
  • _myprogram.aleo (starts with underscore)
  • 123program.aleo (starts with number)
  • my-program.aleo (contains hyphen)
  • myaleo.aleo (contains “aleo”)

Variable and Function Names

Follow snake_case conventions:
let user_balance: u64 = 1000u64;

fn calculate_total_amount(a: u64, b: u64) -> u64 {
    return a + b;
}

Importing Dependencies

Local Dependencies

Import from local packages:
import child.aleo;

program parent.aleo {
    fn main() -> child.aleo/Foo {
        return child.aleo/Foo { x: 0, y: 0 };
    }
}

Network Dependencies

Import from the Aleo network:
import credits.aleo;

program my_program.aleo {
    fn use_credits() {
        // Access credits.aleo functions
    }
}
Network dependencies are automatically fetched and cached in ~/.aleo/registry/.

Special Variables

Leo provides special context variables:
  • self.signer - The address that signed the transaction
  • self.caller - The program that called this function
record Receipt {
    owner: address,
    issuer: address,
}

fn create_receipt() -> Receipt {
    return Receipt {
        owner: self.signer,  // Transaction signer
        issuer: self.caller,  // Calling program
    };
}

Best Practices Summary

1

Use Type Annotations

Always specify types explicitly for clarity:
let amount: u64 = 100u64;  // Good
let amount = 100u64;        // Works, but less clear
2

Initialize Before Use

Declare and initialize variables together:
let total: u64 = 0u64;
total = total + amount;
3

Use Meaningful Names

Choose descriptive names for functions and variables:
fn calculate_user_reward(stake: u64, rate: u64) -> u64  // Good
fn calc(x: u64, y: u64) -> u64                          // Avoid
4

Document Complex Logic

Add comments for non-obvious code:
// Calculate compound interest over n periods
let interest: u64 = principal * rate / 100u64;

Next Steps

Now that you understand Leo program structure: