Skip to main content

Overview

Thank you for your interest in contributing to Leo! This guide will help you get started with contributing to the Leo programming language and its ecosystem.
The Leo compiler is written in Rust and follows strict coding standards to ensure reliability and security.

Getting Started

Prerequisites

  1. Rust Toolchain
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustup update
    
  2. Nightly Rust (for formatting)
    rustup install nightly
    
  3. Git
    git --version
    

Fork and Clone

  1. Fork the repository on GitHub
  2. Clone your fork:
    git clone https://github.com/YOUR_USERNAME/leo
    cd leo
    
  3. Add upstream remote:
    git remote add upstream https://github.com/ProvableHQ/leo
    

Build from Source

# Build all crates
cargo build --release

# Install leo CLI
cargo install --path crates/leo

# Verify installation
leo --version

Pull Request Process

Branch from Mainnet

Ensure your branch is forked from the current mainnet branch:
git checkout mainnet
git pull upstream mainnet
git checkout -b feature/my-feature

Making Changes

  1. Write your code
    • Follow the coding conventions (see below)
    • Add tests for new functionality
    • Update documentation as needed
  2. Format your code
    cargo +nightly fmt --all
    
    Formatting is enforced by a git hook. Commits will fail if code is not properly formatted.
  3. Run clippy
    cargo clippy -- -D warnings
    
    All clippy warnings must be resolved.
  4. Run tests
    cargo test
    

Commit Your Changes

git add .
git commit -m "Add feature: description"
git push origin feature/my-feature

Create Pull Request

  1. Go to your fork on GitHub
  2. Click “New Pull Request”
  3. Fill out the PR template:
    • Link related issues using keywords (e.g., “closes #130”)
    • Describe your changes
    • Explain why the changes are needed
    • List any breaking changes
  4. Wait for review
    • Address feedback promptly
    • Keep your branch up to date with mainnet
    • Be patient and respectful

Coding Conventions

Style Guidelines

Comments

Prefer line comments (//) to block comments (/* ... */). Good:
// A comment on an item.
struct Foo { ... }

fn foo() {} // A comment after an item.

pub fn foo(/* a comment before an argument */ x: T) {...}
Comment Format:
  • Complete sentences with capitalization and punctuation
  • Single space before and after inline block comments
  • No trailing whitespace

Imports

Imports should be:
  • Stated at the top of the file
  • Ordered alphabetically
  • Split into two sections:
Section 1: First-party imports
  • crate imports
  • Leo crates (leo_*)
  • Aleo crates (e.g., snarkvm)
Section 2: Third-party imports
  • Rust std
  • External dependencies
Example:
use crate::Circuit;
use leo_ast::IntegerType;
use snarkvm::prelude::*;

use serde::Serialize;
use std::{
    fmt,
    sync::{Arc, Weak},
};
cargo fmt will automatically sort imports within each section.

Performance and Memory Guidelines

Leo is a large project. Following these guidelines helps ensure high performance and efficient memory usage.

Memory Handling

Pre-allocate Collections

Good:
let mut vec = Vec::with_capacity(100);
let mut map = HashMap::with_capacity(50);
Bad:
let mut vec = Vec::new();  // Will reallocate multiple times
Pre-allocation reduces system calls and ensures optimal memory usage.

Create Collections Just-in-Time

Good:
fn process() {
    // ... other work ...
    
    let mut results = Vec::with_capacity(10);
    for item in items {
        results.push(process_item(item));
    }
    results
}
Bad:
fn process() {
    let mut results = Vec::with_capacity(10);  // Created too early
    
    // ... lots of other work ...
    // (results occupies memory unnecessarily)
    
    for item in items {
        results.push(process_item(item));
    }
    results
}

Use Iterators

Avoid intermediate vectors when possible: Good:
fn process(items: &[Item]) -> impl Iterator<Item = Result> + '_ {
    items.iter().map(|item| process_item(item))
}
Bad:
fn process(items: &[Item]) -> Vec<Result> {
    items.iter()
        .map(|item| process_item(item))
        .collect()  // Unnecessary allocation if caller just iterates
}

Bulk Operations

Good:
vec![x; 100]  // Creates vector with 100 copies of x
vec.resize(100, x);  // Resizes in one operation
Bad:
for _ in 0..100 {
    vec.push(x.clone());  // Many individual operations
}

Pass by Value When Consumed

Good:
fn consume(value: BigStruct) {
    // value is moved in, no clone needed
    do_something(value);
}
Bad:
fn consume(value: &BigStruct) {
    // Caller has to clone if they want to pass ownership
    do_something(value.clone());
}
Pass by value when the function will consume the argument.

Use Copy-on-Write

For slices that may or may not need extending:
use std::borrow::Cow;

fn maybe_extend(slice: &[u8], extend: bool) -> Cow<'_, [u8]> {
    if extend {
        let mut vec = slice.to_vec();
        vec.push(0);
        Cow::Owned(vec)
    } else {
        Cow::Borrowed(slice)
    }
}
This avoids allocation when extension isn’t needed.

Prefer Arrays and Slices

Good:
let arr: [u8; 32] = [0; 32];  // Stack-allocated
let slice: &[u8] = &[1, 2, 3];  // No allocation
Bad:
let vec: Vec<u8> = vec![0; 32];  // Heap-allocated
Use arrays when size is known and not too large.

Avoid Unnecessary Clones

Good:
fn get_name(&self) -> &str {
    &self.name
}
Bad:
fn get_name(&self) -> String {
    self.name.clone()  // Unnecessary allocation
}

Use into_iter() Over iter().cloned()

Good:
let values: Vec<_> = vec.into_iter().collect();
Bad:
let values: Vec<_> = vec.iter().cloned().collect();
When you can consume the collection, into_iter() avoids clones.

Reuse Collections

Good:
let mut buffer = Vec::with_capacity(100);
for _ in 0..10 {
    buffer.clear();
    // ... fill buffer ...
    process(&buffer);
}
Bad:
for _ in 0..10 {
    let mut buffer = Vec::with_capacity(100);  // Reallocates each time
    // ... fill buffer ...
    process(&buffer);
}

Box Large Enum Variants

Good:
enum Token {
    Small(u32),
    Large(Box<LargeStruct>),  // Boxed to keep enum size reasonable
}
Bad:
enum Token {
    Small(u32),
    Large(LargeStruct),  // Makes entire enum large
}

Performance

Avoid format!()

Good:
let s = value.to_string();
Bad:
let s = format!("{}", value);  // Slower than to_string()

Check-Then-Insert Pattern

Good:
use std::collections::hash_map::Entry;

match map.entry(key) {
    Entry::Vacant(e) => { e.insert(value); },
    Entry::Occupied(_) => { /* already exists */ },
}
Bad:
if !map.contains_key(&key) {
    map.insert(key, value);  // Double lookup
}

Use Slice Types in Parameters

Good:
fn process(items: &[Item]) { ... }  // Accepts Vec, array, slice
fn process_str(s: &str) { ... }     // Accepts String, &str
fn process_path(p: &Path) { ... }   // Accepts PathBuf, &Path
Bad:
fn process(items: &Vec<Item>) { ... }    // Only accepts Vec
fn process_str(s: &String) { ... }       // Only accepts String
fn process_path(p: &PathBuf) { ... }     // Only accepts PathBuf

Custom PartialEq and Hash

For structs with distinctive fields:
struct User {
    id: u64,
    name: String,
    email: String,
}

impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id  // Only compare ID
    }
}

impl Hash for User {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);  // Only hash ID
    }
}
This makes comparisons and hashing much faster.

Testing

Run Tests

# Run all tests
cargo test

# Run specific crate tests
cargo test -p leo-parser

# Run with filter
TEST_FILTER=loop cargo test

# Update expectations
UPDATE_EXPECT=1 cargo test

Write Tests

For new features, add tests in tests/tests/ with expectations in tests/expectations/:
#[test]
fn test_new_feature() {
    // Test code here
}

Test Framework

Leo uses expectation testing:
  • Tests in tests/tests/{category}/
  • Expected outputs in tests/expectations/{category}/
  • Run UPDATE_EXPECT=1 cargo test to update expectations

Validation Checklist

Before submitting your PR, ensure:
  • Code compiles: cargo check --all
  • No clippy warnings: cargo clippy -- -D warnings
  • Code is formatted: cargo +nightly fmt --check
  • All tests pass: cargo test
  • Documentation is updated
  • Commit messages are descriptive
  • PR description is complete

Code Review

Review Checklist

Reviewers will check: Correctness:
  • Logic is sound
  • Edge cases are handled
  • No panics in production code
  • Error handling is appropriate
Compiler-Specific:
  • Spans are preserved for error reporting
  • NodeIDs are assigned correctly
  • Pass ordering is respected
  • Generated Aleo instructions are valid
Performance:
  • No unnecessary allocations
  • Pre-allocation where size is known
  • No unnecessary clones
  • Efficient iterators
Security:
  • Input validation at boundaries
  • No information leakage
  • Fail-closed on uncertainty

Architecture Overview

Leo compilation pipeline:
Source (.leo)

Lexer (logos)

Rowan Parse Tree

AST

Passes:
  - Type Checking
  - Loop Unrolling
  - Optimization
  - Code Generation

Aleo Instructions

Key Crates

  • leo-span: Source locations and spans
  • leo-errors: All error types
  • leo-ast: AST node definitions
  • leo-parser-rowan: Lexer and parser
  • leo-parser: Rowan to AST conversion
  • leo-passes: Compiler passes (~25 passes)
  • leo-compiler: Orchestrates compilation
  • leo-package: Project structure
  • leo-fmt: Code formatter
For more details, see AGENTS.md in the repository.

Git Workflow

Commit Messages

Write clear, descriptive commit messages: Good:
Add support for nested tuple destructuring

Implements destructuring for tuples nested within tuples.
Closes #123.
Bad:
fix bug

Squashing Commits

Maintainers may ask you to squash commits:
git rebase -i HEAD~3  # Squash last 3 commits

Keeping Up to Date

git fetch upstream
git rebase upstream/mainnet

Getting Help

Discord

Join the Aleo Discord for:
  • Questions about contributing
  • Discussion of proposed changes
  • Community support

GitHub Discussions

Use GitHub Discussions for:
  • Feature proposals
  • Design discussions
  • General questions

Office Hours

Check the Discord for community office hours and developer meetups.

What to Contribute

Good First Issues

Look for issues labeled good first issue on GitHub:

Ideas

  • Bug fixes
  • Documentation improvements
  • Example programs
  • Test coverage
  • Performance optimizations
  • Error message improvements
  • Developer tooling

What Requires Approval

Before starting work on these, open an issue for discussion:
  • New crates
  • New dependencies
  • New abstractions or traits
  • New error types
  • Breaking changes
  • Major refactors

License

By contributing to Leo, you agree that your contributions will be licensed under the GNU General Public License v3.0.

Code of Conduct

Be respectful, professional, and constructive:
  • Respect differing viewpoints
  • Accept constructive criticism gracefully
  • Focus on what’s best for the community
  • Show empathy towards others

GitHub Repository

Leo source code

Discord Community

Join the discussion
Thank you for contributing to Leo!