Skip to content

v4.0.0

Choose a tag to compare

@github-actions github-actions released this 31 Mar 19:35
d9207de

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 test no longer downloads snarkVM parameters or generates proofs unless --prove is explicitly passed, making test iteration dramatically faster.
  • Non-zero exit status on failure. leo test now 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