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:
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" : "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:
Regular Functions
Standard functions that execute on-chain: fn add(a: u32, b: u32) -> u32 {
return a + b;
}
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;
}
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
Use Type Annotations
Always specify types explicitly for clarity: let amount: u64 = 100u64; // Good
let amount = 100u64; // Works, but less clear
Initialize Before Use
Declare and initialize variables together: let total: u64 = 0u64;
total = total + amount;
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
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: