Thank you for your interest in contributing to Leo! Below you can find some guidelines that the project strives to follow.
Please follow the instructions below when filing pull requests:
- Ensure that your branch is forked from the current mainnet branch.
- Fill out the provided markdown template for the feature or proposal. Be sure to link the pull request to any issues by using keywords. Example: "closes #130".
- Run
cargo fmtbefore you commit; we use thenightlyversion ofrustfmtto format the code, so you'll need to have thenightlytoolchain installed on your machine; there's a git hook that ensures proper formatting before any commits can be made, andrustfmt.tomlspecifies some of the formatting conventions. - Run
cargo clippyto ensure that popular correctness and performance pitfalls are avoided.
Tests live in tests/tests/{category}/ with expectations in tests/expectations/{category}/.
cargo test # Run all workspace tests
cargo test -p leo-parser # Run a single crate's tests
UPDATE_EXPECT=1 cargo test -p leo-parser # Update parser expectations
TEST_FILTER=loop cargo test # Filter by nameCI uses cargo nextest run, which also works locally:
cargo nextest run # Run all workspace tests
cargo nextest run -p leo-parser # Run a single crate's tests- Tests in
tests/tests/cli/with scripts that runleosubcommands end-to-end. - Integration test runner in
leo/tests/integration.rsspins upleo devnode. - 17 test cases cover build, run, deploy, add, and related commands.
- These tests require a running devnode and are gated by environment (not run in standard
cargo test).
Run in order:
cargo check
cargo clippy -- -D warnings
cargo +nightly fmt --check
cargo testClippy warnings are errors. Formatting requires nightly (cargo +nightly fmt --all to fix).
These guidelines ensure consistent readable rust code within the Leo repository.
Prefer line comments (//) to block comments (/* ... */).
When using single-line block comments there should be a single space after the opening sigil and before the closing sigil. Multi-line block comments should have a newline after the opening sigil and before the closing sigil.
Prefer to put a comment on its own line. Where a comment follows code, there should be a single space before it. Where a block comment is inline, there should be surrounding whitespace as if it were an identifier or keyword. There should be no trailing whitespace after a comment or at the end of any line in a multi-line comment. Examples:
// A comment on an item.
struct Foo { ... }
fn foo() {} // A comment after an item.
pub fn foo(/* a comment before an argument */ x: T) {...}Comments should be complete sentences. Start with a capital letter, end with a period (.). An inline block comment may be treated as a note without punctuation.
- Stated at the top of the file
- Ordered alphabetically
- Split in two sections
- First party: crate imports + Aleo imports (example: snarkVM)
- Third party: rust std + everything else
Example:
use crate::Circuit;
use leo_ast::IntegerType;
use serde::Serialize;
use std::{
fmt,
sync::{Arc, Weak},
};rust fmt should automatically sort imports alphabetically after they are split into the appropriate sections.
Leo errors are rendered with ariadne and follow rustc's voice conventions. When adding or editing an error, match the existing style so output stays consistent.
Error builder functions live next to the pass that emits them (e.g. crates/passes/src/errors/type_checker.rs, crates/parser/src/errors.rs) and return Formatted or Backtraced from leo-errors. The builder API is in crates/errors/src/common/formatted.rs.
-
Primary message the string passed to
Formatted::error(...)/Formatted::warning(...)(or theBacktracedequivalents):- Start with a lowercase letter. Proper nouns and code identifiers in backticks keep their case.
- No trailing period.
- Describe what is wrong, not what to do. Use
cannot assign to const input \x`rather thanCannot assign to const input `x`.`. - Quote identifiers and keywords with backticks:
`let`,`foo`.
-
Help text
.with_help(...):- Full sentence(s). Capitalized first letter. Trailing period.
- Imperative voice: "Use
letinstead.", "Rename the field to avoid the clash." - When you know a concrete fix, show it (in backticks or a short example), don't hedge with "Consider …".
-
Note text
.with_note(...):- Same shape as help (full sentences, capitalized, trailing period), but for context rather than fixes.
- Use this when explaining why a rule exists (e.g. "Bidi override characters can disguise source code and are rejected to prevent "trojan source" attacks.").
-
Labels
.with_label(Label::new(span).with_message(...)):- Lowercase, no trailing period, same rules as the primary message.
- Describe the role of the span: "expected here", "previously defined here".
-
Avoid:
- Vague help like "Consider using explicit type annotations.", be specific (
"Add a type annotation, e.g. \let x: u32 = ...;`, so the type can be inferred."`). - Restating the primary message in the help.
- Trailing periods in primary messages or labels; missing periods in help/note.
- Vague help like "Consider using explicit type annotations.", be specific (
Formatted::error(CODE_PREFIX, CODE_MASK + 2, format!("cannot assign to const variable `{var}`"), span)
.with_help(format!("Declare `{var}` with `let` instead of `const` to make it mutable."))Renders as:
[ETYC0372002] Error: cannot assign to const variable `x`
╭─[ main.leo:5:5 ]
│
5 │ x = 10;
│ ─
│
│ Help: Declare `x` with `let` instead of `const` to make it mutable.
───╯
Leo is a big project, so (non-)adherence to best practices related to performance can have a considerable impact; below are the rules we try to follow at all times in order to ensure high quality of the code:
- If the final size is known, pre-allocate the collections (
Vec,HashMapetc.) usingwith_capacityorreserve- this ensures that there are both fewer allocations (which involve system calls) and that the final allocated capacity is as close to the required size as possible. - Create the collections right before they are populated/used, as opposed to e.g. creating a few big ones at the beginning of a function and only using them later on; this reduces the amount of time they occupy memory.
- If an intermediate vector is avoidable, use an
Iteratorinstead; most of the time this just amounts to omitting the call to.collect()if a single-pass iteration follows afterwards, or returning animpl Iterator<Item = T>from a function when the caller only needs to iterate over that result once. - When possible, fill/resize collections "in bulk" instead of pushing a single element in a loop; this is usually (but not always) detected by
clippy, suggesting to create vectors containing a repeated value withvec![x; N]or extending them with.resize(N, x). - When a value is to eventually be consumed in a chain of function calls, pass it by value instead of by reference; this has the following benefits:
- It makes the fact that the value is needed by value clear to the caller, who can then potentially reclaim it from the object afterwards if it is "heavy", limiting allocations.
- It often enables the value to be cloned fewer times (whenever it's no longer needed at the callsite).
- When the value is consumed and is not needed afterwards, the memory it occupies is freed, improving memory utilization.
- If a slice may or may not be extended (which requires a promotion to a vector) and does not need to be consumed afterwards, consider using a
Cow<'a, [T]>combined withCow::to_mutinstead to potentially avoid an extra allocation; an example in Leo could be conditional padding of bits. - Prefer arrays and temporary slices to vectors where possible; arrays are often a good choice if their final size is known in advance and isn't too great (as they are stack-bound), and a small temporary slice
&[x, y, z]is preferable to avec![x, y, z]if it's applicable. - If a reference is sufficient, don't use
.clone()/to_vec(), which is often the case with methods onstructs that provide access to their contents; if they only need to be referenced, there's no need for the extra allocation. - Use
into_iter()instead ofiter().cloned()where possible, i.e. whenever the values being iterated over can be consumed altogether. - If possible, reuse collections; an example would be a loop that needs a clean vector on each iteration: instead of creating and allocating it over and over, create it before the loop and use
.clear()on every iteration instead. - Try to keep the sizes of
enumvariants uniform; useBox<T>on ones that are large.
- Avoid the
format!()macro; if it is used only to convert a single value to aString, use.to_string()instead, which is also available to all the implementors ofDisplay. - Don't check if an element belongs to a map (using
containsorget) if you want to conditionally insert it too, as the return value ofinsertalready indicates whether the value was present or not; use that or theEntryAPI instead. - If a reference is sufficient as a function parameter, use:
&[T]instead of&Vec<T>&strinstead of&String&Pathinstead of&PathBuf
- For
structs that can be compared/discerned based on some specific field(s), consider hand-written implementations ofPartialEqandHash(they must match) for faster comparison and hashing.
- Boundary conditions handled: zero, empty, max, off-by-one.
- Error handling correct; no panics in production paths.
- AST transformations preserve semantics.
- Spans preserved through transformations for error reporting.
- NodeIDs assigned correctly for new nodes.
- Pass ordering dependencies respected.
- Generated Aleo instructions are valid.
- No unnecessary allocations in hot paths.
- Pre-allocation with
with_capacitywhere size known. - No unnecessary
.clone()- prefer references. - Iterators used efficiently; no intermediate collections.
- Input validation at trust boundaries.
- No information leakage in error messages.
- Fail-closed (reject on uncertainty).