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) { }
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};. Returning an output ends the execution of the function, and the type of the returned value must match the output type declared 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;
}
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 = self.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 = self.caller;
return (new, final {
decrement_balance(caller, amount);
});
}
fn burn(public amount: u64) -> Final {
let caller: address = self.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.
View Functions
A view fn is a read-only entry point. It is declared inside a program {} block with the view modifier and exposes a query that can be evaluated by a node without producing a transaction.
program vault.aleo {
mapping balances: address => u64;
// A `view fn` is a read-only entry point. It can read mappings, storage,
// vectors, `block.height`, and `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 = self.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, block.height, and 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 may call a helper
fn(its body is fully inlined into the view), but it cannotcallanotherview fn, afinal fn, or an entry point. This keeps the emitted Aleoviewblock free ofcallinstructions, which snarkVM requires. Dynamic calls (thedyn ...form) are also rejected. - No
block.timestamp,Snark::verify,Snark::verify_batch, orprogram_owner— these are available infinal {}but not when a node evaluates a view off-consensus. - 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() {}
}
Unlike a helper fn (which is inlined at its call site), a view fn remains a separate callable entity, and each invocation from the final {} block re-runs the view's body.
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() {}
}
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, and address.
Const generic parameters are only valid on inlinable helper fn functions. They are not permitted on entry point functions inside a program {} block, final fn functions, functions annotated with @no_inline, or function signatures declared inside an interface.
The @no_inline Annotation
By default the compiler inlines a helper fn whenever inlining is safe and beneficial — most commonly when the function is called only once, takes no arguments, or all of its arguments have empty types. Inlining reduces call overhead and shrinks the compiled program.
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 the function is intentionally shared across multiple call sites but the compiler would otherwise duplicate it, or when you want to preserve the function boundary for readability 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 a recognized annotation name, but no compiler pass acts on it — it is a silent no-op carried over from earlier Leo versions, where inline was a function-modifier keyword rather than an annotation (see Migrating from Leo 3.5 to 4.0). The default inlining behaviour described above is the same whether or not @inline is present, so prefer to leave it out of 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.