Structure of a Leo Program
Layout of a Leo Program
A Leo program contains declarations of a Program, a Constructor, Constants, Imports , Structs, Records, Mappings, Interfaces, and functions. Declarations are locally accessible within a program file. If you need a declaration from another Leo file, you must import it.
Program
A program is a collection of code (its functions) and data (its types) that resides at a program ID on the Aleo blockchain. A program is declared as program {name}.{network} { ... }, with the body delimited by curly braces.
For the canonical list of which declarations belong inside vs. Outside the program { ... } block, the program-ID naming rules, and import semantics, see Project Layout.
import foo.aleo;
const FOO: u64 = 1u64;
struct Message {
sender: address,
object: u64,
}
fn compute(a: u64, b: u64) -> u64 {
return a + b + FOO;
}
program hello.aleo {
mapping account: address => u64;
record Token {
owner: address,
amount: u64,
}
fn mint_public(
public receiver: address,
public amount: u64,
) -> (Token, Final) {
let token: Token = Token { owner: receiver, amount };
return (token, final {
let current_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current_amount + amount);
});
}
@noupgrade
constructor() {}
}
Constructor
A constructor is a special, mandatory function in the program { ... } block. Declare it as constructor() { ... }. Each program must declare exactly one constructor.
It has no parameters or return value, and it is not a regular fn. Do not call it directly. The network runs it on-chain during the initial deployment and each upgrade. It controls the program upgrade policy.
Two properties set a constructor apart from an ordinary function:
- Immutable. The logic set at first deployment can never be changed, modified, or deleted by a future upgrade.
- Policy-bearing. It carries exactly one upgrade annotation —
@noupgrade,@admin,@checksum, or@custom— that selects how the program may be upgraded. The managed modes (@noupgrade,@admin,@checksum) require an empty body, since the compiler generates their logic.@customrequires a non-empty body that you write yourself. A constructor with no annotation, or with more than one, is a compile error.
// The 'noupgrade_example' program.
program noupgrade_example.aleo {
// This constructor is for the "noupgrade" mode.
// It is immutable and prevents any future upgrades.
@noupgrade
constructor() {
// The Leo compiler automatically generates the constructor logic.
}
fn main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
Inside a constructor, you can read on-chain program metadata through the std::ctx module — namely std::ctx::addr(), std::ctx::edition(), std::ctx::program_owner(), and std::ctx::checksum(). A @custom constructor typically branches on std::ctx::edition() to apply different rules at first deployment (edition == 0) versus later upgrades:
// The 'timelock_example' program.
program timelock_example.aleo {
@custom
constructor() {
// For upgrades (edition > 0), enforce a block height condition on when the constructor can be called successfully
if std::ctx::edition() > 0u16 {
assert(std::ctx::block_height() >= 1300u32);
}
}
fn main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}
For the annotation argument grammar, the meaning of each std::ctx::*() accessor, and worked patterns for every upgrade mode, see the Upgrading Programs guide.
Constant
A constant is declared as const {name}: {type} = {expression};.
Constants are immutable, and the right-hand side must be an expression evaluatable at compile time.
Constants can be declared in three scopes:
- Global scope (outside the
programblock inmain.leo): accessible anywhere in the same file. - Local scope (inside a function body): accessible only within that function.
- Module scope: applies to each non-
main.leosource file in the package. Module files do not contain aprogramblock. They can only declareconst,struct,fn, andinterface. Usepath::to::module::CONST_NAMEto access the constant in the same package. See Modules.
Constants are also supported in libraries, which are separate packages containing reusable code. A library's root file and its submodules may declare constants, accessible from any dependent package as library::CONST_NAME or library::path::to::submodule::CONST_NAME.
Accessibility across packages: An importing program can access global constants with program_name.aleo::CONST_NAME. Use program_name.aleo::path::to::submodule::CONST_NAME to access a constant in an imported program submodule. This access requires a dependency compiled from Leo source. Precompiled .aleo stubs do not contain the submodule type information that resolution requires.
const MAX: u64 = 100u64; // global constant
const MULTIPLIER: u64 = 2u64; // another global constant
program constants_demo.aleo {
fn compute(x: u64) -> u64 {
const OFFSET: u64 = 5u64; // local constant
return x * MULTIPLIER + OFFSET;
}
@noupgrade
constructor() {}
}
Supported types: Constants support all integer types, bool, field, group, scalar, and address. They also support tuples, arrays, and structs composed of these types.
Compile-time expressions: The right-hand side of a constant declaration must be evaluatable at compile time. Valid right-hand sides include:
- Literal values (for example,
42u32,true,1field) - References to previously declared constants
- Arithmetic, bitwise, and comparison expressions over constants (for example,
MAX * 2u64,!FLAG) - Tuple, array, and struct expressions whose components are themselves compile-time constants
const BASE: u32 = 10u32;
const LIMIT: u32 = BASE * 5u32; // expression over constants
const PAIR: (u32, bool) = (LIMIT, true); // tuple constant
Import
An import is declared as import {filename}.aleo;. The dependency resolver pulls the imported program from the network or the local imports/ directory. See Imports for the declaration syntax and the Dependencies guide for resolution rules.
import foo.aleo; // Import all `foo.aleo` declarations into the `hello.aleo` program.
program hello.aleo {
Mappings
A mapping is declared as mapping {name}: {key-type} => {value-type}.
Mappings contain key-value pairs and are stored on chain.
// On-chain storage of an `account` mapping,
// with `address` as the type of keys,
// and `u64` as the type of values.
mapping account: address => u64;
Storage
A storage variable is declared as storage {name}: {type}. Storage variables contain singleton values. They are declared at program scope and are stored on chain, similar to mappings.
// On-chain storage of an `counter` storage variable of type u32,
storage counter: u32;
A storage vector is declared as storage {name}: [{type}]. Storage vectors contain dynamic lists of values of a given type. They are declared at program scope and are stored on chain, similar to mappings.
// On-chain storage of an `accounts` storage vector of type address,
storage accounts: [address];
Struct
A struct data type is declared as struct {name} {}.
Structs contain component declarations {name}: {type},.
struct Array3 {
a0: u32,
a1: u32,
a2: u32,
}
Record
A record data type is declared as record {name} {}. A record name must not contain aleo. It must not prefix another record name declared in the same program. This check does not apply across imported programs. It is a snarkVM requirement.
Records contain component declarations {visibility} {name}: {type},. Names of record components must not contain the keyword aleo.
The visibility qualifier may be specified as constant, public, or private. If no qualifier is provided, Leo defaults to private.
Each record must contain an owner component of type address, as shown below. A record function input also requires the _nonce: group and _version: u8 components. Do not declare these components in the Leo program. The compiler inserts them automatically.
record Token {
// The token owner.
owner: address,
// The token amount.
amount: u64,
}