v4.0.0
Leo 4.0 is a major release that streamlines the language model, introduces first-class library and interface support, and ships full dynamic dispatch. The on-chain execution model has been simplified, the syntax made more consistent, and the foundation laid for large-scale program composition on Aleo.
Unified fn Syntax
The biggest surface-level change in 4.0 is the unification of all function declarations under a single fn keyword. The old transition, function, inline, and async transition keywords are gone.
| Leo 3.5 | Leo 4.0 |
|---|---|
transition foo() |
fn foo() inside program {} |
async transition foo() -> Future |
fn foo() -> Final inside program {} |
function foo() |
fn foo() outside program {} |
inline foo() |
fn foo() outside program {} |
async function foo() |
final { ... } block |
async { ... } |
final { ... } |
f.await() |
f.run() |
Future |
Final |
@test script foo() |
@test fn foo() |
Entry functions live inside program {} and form the public interface of the program. Helper functions live outside program {} and cannot produce records. Functions that need on-chain logic include a final { } block — everything inside it runs atomically on the network.
// Helper function — outside program {}
fn add(a: u64, b: u64) -> u64 {
return a + b;
}
program counter.aleo {
mapping tally: address => u64;
// Entry function — inside program {}
fn increment(amount: u64) -> Final {
let sum: u64 = add(tally[self.caller], amount);
return final {
// On-chain logic
tally[self.caller] += amount;
};
}
}Interfaces
Leo 4.0 introduces interfaces — a compile-time mechanism for defining structural contracts over programs. An interface declares the functions, records, mappings, and storage that a conforming program must provide. Programs opt in with : InterfaceName.
// Declared outside program {}
interface Transfer {
record Token;
fn transfer(input: Token, to: address, amount: u64) -> Token;
}
interface Pausable {
fn pause();
fn unpause();
}
// Implements both interfaces
program my_token.aleo : Transfer + Pausable {
record Token {
owner: address,
amount: u64,
}
fn transfer(input: Token, to: address, amount: u64) -> Token {
return Token { owner: to, amount };
}
fn pause() { ... }
fn unpause() { ... }
}Interfaces support inheritance — an interface can extend one or more other interfaces:
interface Token : Transfer + Balances {
fn mint(to: address, amount: u64);
}Record requirements in interfaces can be structural (with optional .. to allow extra fields):
interface HasMemo {
record Rec { owner: address, memo: u64, .. }
}Dynamic Dispatch
Programs can now call other programs without knowing their concrete identity at compile time. Dynamic dispatch uses the Interface@(target)::method(args) syntax, where target is a value of the new identifier type. Identifier literals use single-quote syntax.
interface Counter {
fn increment(amount: u64) -> u64;
}
program dispatcher.aleo {
fn run(target: identifier, amount: u64) -> u64 {
// Calls `increment` on whichever program `target` names
return Counter@(target)::increment(amount);
}
}
// Calling with a literal identifier
let target: identifier = 'my_counter';Records passed through dynamic calls are typed as dyn record — a record whose structure is not known at compile time. Fields can be accessed by name and are resolved at runtime.
fn get_memo(rec: dyn record) -> u64 {
return rec.memo; // Fails at runtime if the field is absent
}Leo Libraries
Programs can now be packaged as libraries — reusable collections of constants, structs, and functions with no on-chain footprint. Libraries are created with leo new --library, use lib.leo instead of main.leo, and contain no program {} block.
// math_utils/src/lib.leo
const PI: field = 3141592653field;
struct Point {
x: i64,
y: i64,
}
fn distance(a: Point, b: Point) -> field {
// ...
}Libraries are referenced by path with :: — no import statement is needed, just a program.json dependency entry:
// Consumer program
fn move_point(p: math_utils::Point, dx: i64) -> math_utils::Point {
return math_utils::Point { x: p.x + dx, y: p.y };
}Libraries support submodules (math_utils::geometry::area) and generic functions (fn dot::[N: u32](a: Vector, b: Vector) -> field). They are fully inlined at compile time.
External Path Separator: / → ::
All external paths now use :: instead of /:
// Leo 3.5
token.aleo/transfer(to, amount)
// Leo 4.0
token.aleo::transfer(to, amount)This applies to external function calls, external storage access (token.aleo::balances), and library item paths.
Removed: Scripts and leo debug
Script functions and leo debug have been removed. In Leo 3.5, @test script functions provided an interactive debugging entrypoint invoked via leo debug. This feature has been retired in 4.0. Tests are now written as @test fn functions inside program {} and run with leo test.
// Leo 3.5 — removed
@test script run() { ... }
// Leo 4.0
program my_program.aleo {
@test fn run() { ... }
}leo test Improvements
- Proof generation is skipped by default.
leo testno longer downloads snarkVM parameters or generates proofs unless--proveis explicitly passed, making test iteration dramatically faster. - Non-zero exit status on failure.
leo testnow correctly exits with a non-zero code when any test fails, enabling proper CI integration.
leo test # Fast — no proof generation
leo test --prove # Full end-to-end proof generation