Functions
Entry Functions
Entry functions in Leo are declared as fn {name}() {} inside a program {} block. They define the program's public interface and can be called directly when running a Leo program (via leo run). If they include a final { } block to execute code on-chain, they must return Final.
program hello.aleo {
fn foo(
public a: field,
b: field,
) -> field {
return a + b;
}
@noupgrade
constructor() {}
}
Inputs
Inputs are declared as {visibility} {name}: {type}. They must be declared just after the function name declaration, in parentheses.
// The entry function `foo` takes a single input `a` with type `field` and visibility `public`.
fn foo(public a: field) { }
A visibility modifier may not be applied to a record or Final parameter. Records are passed by their .record marker and Finals carry no visibility, so a public or private mode on them is meaningless and is rejected.
Outputs
The return type of the function is declared as -> {expression} and must be declared just after the function inputs. A function output is calculated as return {expression};. A return operation ends the function. The returned value type must match the output type in the function signature.
fn foo(public a: field) -> field {
// Returns the addition of the public input a and the value `1field`.
return a + 1field;
}
As with inputs, a record or Final output cannot carry a visibility modifier.
On-chain State with final { }
A final { } block is used to define computation that gets executed on-chain. The most common use case is to initiate or change public on-chain state within mappings or storage.
An entry fn that includes on-chain logic returns Final and embeds the on-chain code in a final { } block. Final blocks are atomic. They either succeed or fail, and state is reverted on failure.
program transfer.aleo {
record token {
owner: address,
amount: u64,
}
mapping account: address => u64;
// The function `transfer_public_to_private` turns a specified token amount
// from `account` into a token record for the specified receiver.
//
// This function preserves privacy for the receiver's record, however
// it publicly reveals the sender and the specified token amount.
fn transfer_public_to_private(
receiver: address,
public amount: u64
) -> (token, Final) {
// Produce a token record for the token receiver.
let new: token = token {
owner: receiver,
amount,
};
let caller: address = std::ctx::caller();
// Return the receiver's record, then decrement the token amount of the caller publicly.
return (new, final {
// Decrements `account[sender]` by `amount`.
// If `account[sender]` does not exist, it will be created.
// If `account[sender] - amount` underflows, `transfer_public_to_private` is reverted.
let current_amount: u64 = Mapping::get_or_use(account, caller, 0u64);
Mapping::set(account, caller, current_amount - amount);
});
}
@noupgrade
constructor() {}
}
If there is no need to create or alter the public on-chain state, a final { } block is not required.
On-chain State with final fn
When finalization logic is shared across multiple entry functions, it can be extracted into a final fn, declared outside the program {} block. A final fn call must still be wrapped in a final { } block at the call site:
final fn decrement_balance(sender: address, amount: u64) {
let current_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
Mapping::set(account, sender, current_amount - amount);
}
program transfer.aleo {
record token {
owner: address,
amount: u64,
}
mapping account: address => u64;
fn transfer_public_to_private(
receiver: address,
public amount: u64
) -> (token, Final) {
let new: token = token {
owner: receiver,
amount,
};
let caller: address = std::ctx::caller();
return (new, final {
decrement_balance(caller, amount);
});
}
fn burn(public amount: u64) -> Final {
let caller: address = std::ctx::caller();
return final {
decrement_balance(caller, amount);
};
}
@noupgrade
constructor() {}
}
The body of decrement_balance is inlined into each caller's final { } block at compile time — no shared function exists in the compiled output.
A final fn may also declare an output type and return a value, like an ordinary function. The result is bound at the call site inside the final { } block, which is useful for sharing a computed on-chain value across entry functions:
// A `final fn` may declare an output type and return a value.
final fn capped_amount(receiver: address, amount: u64) -> u64 {
let current: u64 = Mapping::get_or_use(account, receiver, 0u64);
if current + amount > 1000u64 {
return 1000u64;
}
return current + amount;
}
program final_fn_return_demo.aleo {
mapping account: address => u64;
fn deposit(public receiver: address, public amount: u64) -> Final {
return final {
// The returned value is bound at the call site.
let credited: u64 = capped_amount(receiver, amount);
Mapping::set(account, receiver, credited);
};
}
@noupgrade
constructor() {}
}
View Functions
A view fn is a read-only entry point. Declare it in a program {} block with the view modifier. A node can evaluate the resultant query without a transaction.
program vault.aleo {
mapping balances: address => u64;
// A `view fn` is a read-only entry point. It can read mappings, storage,
// vectors, `std::ctx::block_height()`, and `std::ctx::network_id()`, but cannot write any state
// or call other functions.
view fn balance_of(account: address) -> u64 {
return balances.get_or_use(account, 0u64);
}
fn deposit(amount: u64) -> Final {
let caller: address = std::ctx::caller();
return final {
let current: u64 = Mapping::get_or_use(balances, caller, 0u64);
Mapping::set(balances, caller, current + amount);
};
}
@noupgrade
constructor() {}
}
A view fn body sees the same on-chain context as a final {} block — it can read mappings, storage, vectors, std::ctx::block_height(), and std::ctx::network_id(). Beyond the final {} rules above, a view adds these restrictions:
- Read-only. All state writes are rejected — both singleton storage assignment (
counter = 5u64;,counter = none;) and the mutating intrinsicsMapping::set,Mapping::remove,Vector::set,Vector::push,Vector::pop,Vector::swap_remove,Vector::clear. - Leaf in the emitted bytecode. A view can call a helper
fn, and Leo puts the helper body in the view. A view cannot call anotherview fn, afinal fn, or an entry point. Thus, the Aleoviewblock has nocallinstructions, as snarkVM requires. The compiler also rejects dynamic calls in thedyn ...form. - On-chain reads and proof verification. A view can use
std::ctx::block_timestamp(),std::ctx::program_owner(),Snark::verify, andSnark::verify_batch. These operations do not change state. - Returns plaintext only (no records). Cannot be combined with
final.
Calling Views from On-chain Code
view fns are only callable from a finalize context — a final {} block, a final fn helper, or a hoisted finalize body. A plain entry-function body cannot call a view directly.
program vault.aleo {
mapping balances: address => u64;
mapping totals: address => u64;
view fn get_balance(account: address) -> u64 {
return balances.get_or_use(account, 0u64);
}
// A `final {}` block may call a same-program `view fn`. Unlike a helper
// `fn` (which is inlined), the view body is run as a separate invocation
// each time the `final {}` block executes.
fn cache_total(account: address) -> Final {
return final {
let bal: u64 = get_balance(account);
Mapping::set(totals, account, bal + 100u64);
};
}
@noupgrade
constructor() {}
}
Leo puts a helper fn in its call site, but a view fn remains a separate callable entity. Each call from the final {} block runs the view body again.
The same rule applies across programs — a final {} block can call a view fn exposed by an imported program:
import data.aleo;
program cache.aleo {
mapping totals: address => u64;
// A `final {}` block in `cache.aleo` calls a `view fn` exposed by the
// imported `data.aleo` program. Codegen emits a cross-program
// `call data.aleo/get_balance ... into ...` inside the on-chain body.
fn cache_total(account: address) -> Final {
return final {
let bal: u64 = data.aleo::get_balance(account);
Mapping::set(totals, account, bal + 100u64);
};
}
@noupgrade
constructor() {}
}
The Constructor
The constructor is the other function-like declaration in a program {} block. You do not call it directly. The network runs it on-chain during deployment and each upgrade. The constructor enforces the program upgrade policy. See Constructor and the Upgrading Programs guide.
Helper Function
A helper function is declared as fn {name}({arguments}) {} outside the program {} block.
They contain expressions and statements that can compute values, but cannot produce records.
Helper functions cannot be called directly from outside the program. Instead, they are called by entry functions.
Inputs of helper functions cannot have {visibility} modifiers, since they are used only internally, not as part of a program's external interface.
fn foo(
a: field,
b: field,
) -> field {
return a + b;
}
Helper functions also support const generics:
fn sum_first_n_ints::[N: u32]() -> u32 {
let sum = 0u32;
for i in 0u32..N {
sum += i;
}
return sum;
}
program main.aleo {
fn main() -> u32 {
return sum_first_n_ints::[5u32]();
}
@noupgrade
constructor() {}
}
Acceptable types for const generic parameters include integer types, bool, scalar, group, field, address, and identifier.
Const generic parameters are only valid on functions that are inlined at every call site. They are not permitted on entry point functions inside a program {} block, functions annotated with @no_inline, or function signatures declared inside an interface. final fns are always inlined into their final {} callsite, so they may declare const generic parameters.
The @no_inline Annotation
By default, the compiler puts a helper fn in each call site when this operation is safe and beneficial. Common conditions are one call, no arguments, or only arguments with empty types. This operation decreases call overhead and the compiled program size.
To opt out of this default and force a separate AVM function for a helper, annotate it with @no_inline:
@no_inline
fn expensive_helper(a: u32, b: u32) -> u32 {
// ...
return a + b;
}
Use @no_inline when a function is intentionally shared across multiple call sites. You can also use it to keep the function boundary clear in the compiled output.
When @no_inline is ignored
Some helpers cannot exist as standalone AVM functions and must be inlined regardless of the annotation. In these cases the compiler ignores @no_inline and emits a warning at the annotation site:
- helper functions defined in a submodule (
path::nested::fn) — Aleo identifiers are flat, so there is no bytecode form for a nested name, - helper functions defined in a library — libraries have no on-chain footprint,
- a
final fn, - a helper reached from an on-chain context (a
constructoror finalize block), - a helper with more than 16 arguments,
- a helper whose argument or return type names an
Optionaltype, - helpers transitively reachable from another helper that itself must be inlined.
The annotation has no effect on entry fn declarations either — the entry-point boundary is part of the program's public interface and is never inlined away.
The @inline Annotation
The compiler accepts @inline as an annotation name, but no compiler pass acts on it. It is a silent no-op from earlier Leo versions, where inline was a function modifier. See Migrating from Leo 3.5 to 4.0.
The default behavior is the same with or without @inline. Do not put @inline in new code.
Function Call Rules
- An entry
fncan call: helperfns,final fns, and external entryfns. Local entryfns andview fns (outside afinal {}block) are rejected. - A helper
fncan only call: other helperfns. - A
final fncan call: helperfns, otherfinal fns, andview fns. - A
final {}block can call: helperfns,final fns, andview fns (same-program or cross-program). - A
view fncan only call helperfns (which get inlined). Otherview fns,final fns, and entry points are rejected. - Recursive calls (direct or indirect) are not allowed.
You can call a cross-program final fn only when the function and its transitive calls do not write on-chain state. Call an entry function in the other program when the operation must write its state.