Skip to main content
Version: 4.3.0

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 declared inside the program { ... } block as constructor() { ... }. Every program must declare exactly one. It takes no parameters and returns no value, and it is not a regular fn: you never call it directly. Instead, the network runs it on-chain during the program's initial deployment and on every subsequent upgrade, where it acts as the gatekeeper for the program's 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; @custom requires 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 self — namely self.address, self.edition, self.program_owner, and self.checksum. A @custom constructor typically branches on self.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 self.edition > 0u16 {
assert(block.height >= 1300u32);
}
}

fn main(public a: u32, b: u32) -> u32 {
let c: u32 = a + b;
return c;
}
}

For the annotation argument grammar, the type and meaning of each self.* operand, 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 program block in main.leo): accessible anywhere in the same file.
  • Local scope (inside a function body): accessible only within that function.
  • Module scope (any non-main.leo source file in the package; module files do not contain a program block and may only declare const, struct, fn, and interface): accessible within the same package via path::to::module::CONST_NAME. See Modules for details.

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: Global constants in a program are accessible from other programs that import it, using program_name.aleo::CONST_NAME. Constants declared in a submodule of an imported program are reachable through their full module path — program_name.aleo::path::to::submodule::CONST_NAME — provided the dependency is compiled from Leo source (pre-compiled .aleo stubs do not carry the submodule type information needed for resolution).

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: All integer types (u8, u16, u32, u64, u128, i8, i16, i32, i64, i128), bool, field, group, scalar, address, and 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 (e.g., 42u32, true, 1field)
  • References to previously declared constants
  • Arithmetic, bitwise, and comparison expressions over constants (e.g., 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 the keyword aleo, and must not be a prefix of any other record name declared in the same program (the check does not extend across imported programs). This 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.

Record data structures must always contain a component named owner of type address, as shown below. When passing a record as input to a program function, the _nonce: group and _version: u8 components are also required but do not need to be declared in the Leo program. They are inserted automatically by the compiler.

record Token {
// The token owner.
owner: address,
// The token amount.
amount: u64,
}