Skip to main content

Leo Language Guide

Generalities

Statically Typed

Leo is a statically typed language, which means we must know the type of each variable before executing a circuit.

Explicit Types Required

There is no undefined or null value in Leo. When assigning a new variable, the type of the value must be explicitly stated.

Pass by Value

Expressions in Leo are always passed by value, which means their values are always copied when they are used as function inputs or in right sides of assignments.

Data Types and Values

Booleans

Leo supports the traditional true or false boolean values. The explicit bool type for booleans in statements is required.

let b: bool = false;

Integers

Leo supports signed integer types i8, i16, i32, i64, i128 and unsigned integer types u8, u16, u32, u64, u128.

let b: u8 = 1u8;
info

Higher bit length integers generate more constraints in the circuit, which can slow down computation time.

A Note on Leo Integers

Leo will not default to an integer type. The definition of a integer must include an explicit type. Type casting is not yet supported.

let a: u8 = 2u8; // explicit type
let b: u8 = 2; // implicit type -- not supported

Field Elements

Leo supports the field type for the base field elements of the elliptic curve. These are unsigned integers below the modulus of the base field.

let a: field = 1field;
let b: field = 21888242871839275222246405745257275088548364400416034343698204186575808495617field;

Group Elements

The set of affine points on the elliptic curve passed into the Leo compiler forms a group. Leo supports a subgroup of this group, generated by a generator point, as a primitive data type. Group elements are special since their values can be defined from the x-coordinate of a coordinate pair, such as 1group. The group type keyword group must be used when specifying a group value.

let b: group = 0group; // the zero of the group

let a: group = 1group; // the group generator

let c: group = 2group; // 2 * the group generator

Scalar Elements

Leo supports the scalar type for scalar field elements of the elliptic curve subgroup. These are unsigned integers below the modulus of the scalar field.

let a: scalar = 1scalar;

Addresses

Addresses are defined to enable compiler-optimized routines for parsing and operating over addresses. These semantics will be accompanied by a standard library in a future sprint.

let receiver: address = aleo1ezamst4pjgj9zfxqq0fwfj8a4cjuqndmasgata3hggzqygggnyfq6kmyd4;

Layout of a Leo Program

A Leo program contains declarations of a Program Scope, Imports, Transition Functionns, Helper Functions, Structs, Records, Mappings, and Finalize Functions. Declarations are locally accessible within a program file. If you need a declaration from another Leo file, you must import it.

Program Scope

A program scope in the sense of Leo is a collection of code (its functions) and data (its types) that resides at a program ID on the Aleo blockchain.

import foo.leo;

program hello.aleo {
mapping balances: address => u64;

record token {
owner: address,
gates: u64,
amount: u64,
}

struct message {
sender: address,
object: u64,
}

transition mint_public(
public receiver: address,
public amount: u64,
) -> token {
return token {
owner: receiver,
gates: 0u64,
amount,
} then finalize(receiver, amount);
}

finalize mint_public(
public receiver: address,
public amount: u64,
) {
increment(balances, receiver, amount);
}

function compute(a: u64, b: u64) -> u64 {
return a + b;
}
}

The following must be declared inside the program scope in a Leo file:

  • mappings
  • record types
  • struct types
  • transition functions
  • helper functions
  • finalize functions

The following must be declared outside the program scope in a Leo file:

  • imports

Program ID

A program ID is declared as {name}.{network}. Currently, aleo is the only supported network domain.

hello.aleo

Import

You can import dependencies that are downloaded to the imports directory. An import is declared as import {filename}.leo; This will look for imports/{filename}.leo and bring all declarations into the current file scope. If there are duplicate names for declarations, Leo will fail to compile. Ordering is enforced for imports, which must be at the top of file.

caution

Leo imports are unstable and currently only provide minimal functionality. Their syntax is expected to change.

import foo.leo; // Import all `foo.leo` declarations into the `hello.aleo` program.program hello.aleo { }

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} {}. Records contain component declarations {name}: {type},. Record data structures must contain the owner and gates components as shown below. When passing a record as input to a program function, the _nonce: group, component is also required (but it does not need to be declared in the Leo program).

record token {    // The token owner.    owner: address,    // The Aleo balance (in gates).    gates: u64,    // The token amount.    amount: u64,}

Mapping

A mapping is declared as mapping {name}: {key-type} => {value-type}. Mappings contain key-value pairs. Mappings 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;

Transition Function

Transition functions in Leo are declared as transition {name}() {}. Transition functions can be called directly when running a Leo program (via leo run). Transition functions contain expressions and statements that can compute values. Transition functions must be in a program's current scope to be called.

program hello.aleo {    transition foo(        public a: field,        b: field,    ) -> field {        return a + b;    }}

Function Inputs

A function input is declared as {visibility} {name}: {type}, Function inputs must be declared just after the function name declaration, in parentheses.

// The transition function `foo` takes a single input `a` with type `field` and visibility `public`.transition foo(public a: field) { }

Function Outputs

A function output is calculated as return {expression};. Returning an output ends the execution of the function. The return type of the function declaration must match the type of the returned {expression}.

transition foo(public a: field) -> field {    // Returns the addition of the public input a and the value `1field`.    return a + 1field;}

Helper Function

A helper function is declared as function {name}() {}. Helper functions contain expressions and statements that can compute values. Helper functions that cannot be executed directly. Helper functions must be called by other functions. Inputs of helper functions cannot have {visibility} modifiers like transition functions, since they are used only internally, not as part of a program's external interface.

function foo(    a: field,    b: field,) -> field {    return a + b;}

Increment and Decrement

An increment statement has the form increment(mapping, key, value);. A decrement statement has the form decrement(mapping, key, value);. Increment and decrement statements can only be used in finalize functions.

program transfer.aleo {    // On-chain storage of an `account` map,    // with `address` as the key,    // and `u64` as the value.    mapping account: address => u64;    transition transfer_public(...) {...}    finalize transfer_public(        public sender: address,        public receiver: address,        public amount: u64,    ) {        // Decrements `account[sender]` by `amount`.        // If `account[sender]` does not exist, it will be created.        // If `account[sender] - amount` underflows, `transfer_public` is reverted.        decrement(account, sender, amount);        // Increments `account[receiver]` by `amount`.        // If `account[receiver]` does not exist, it will be created.        // If `account[receiver] + amount` overflows, `transfer_public` is reverted.        increment(account, receiver, amount);    }}

Finalize Function

A finalize function is declared as finalize {name}:. A finalize function must immediately followed a transition function, and must have the same name; it is associated with the transition function and is executed on chain, after the zero-knowledge proof of the execution of the associated transition is verified; a finalize function finalizes a transition function on chain. Upon success of the finalize function, the program logic is executed. Upon failure of the finalize function, the program logic is reverted.

program transfer.aleo {    // 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 caller and the specified token amount.    transition transfer_public_to_private(        public receiver: address,        public amount: u64    ) -> token {        // Produce a token record for the token receiver.        let new: token = token {            owner: receiver,            gates: 0u64,            amount,        };        // Return the receiver's record, then decrement the token amount of the caller publicly.        return new then finalize(self.caller, amount);    }    // Decrements `account[owner]` by `amount`.    // If `account[owner]` does not exist, it will be created.    // If `account[owner] - amount` underflows, `transfer_public_to_private` is reverted.    finalize transfer_public_to_private(        public owner: address,        public amount: u64,    ) {        decrement(account, owner, amount);    }}

Operators

Operators in Leo compute a value based off of one or more expressions. Leo will try to detect arithmetic operation errors as soon as possible. If an integer overflow or division by zero can be identified at compile time, Leo will quickly tell the programmer. Otherwise, the error will be caught at proving time when transition function inputs are processed.

For instance, addition adds first with second, storing the outcome in destination. For integer types, a constraint is added to check for overflow. For cases where wrapping semantics are needed for integer types, see the Add Wrapped operator.

let a: u8 = 1u8 + 1u8;
// a is equal to 2

a += 1u8;
// a is now equal to 3

a = a.add(1u8);
// a is now equal to 4

Arithmetic Operators

OperationOperandsSupported Types
addition+ += .add()field group scalar i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping addition.add_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
negation(unary)- .neg()field group i8 i16 i32 i64 i128
subtraction(binary)- -= .sub()field group i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping subtraction(binary).sub_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
multiplication* *= .mul()field group scalar i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping multiplication.mul_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
division/ /= .div()field i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping division.div_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
remainder% %= .rem()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping remainder.rem_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
exponentiation** **= .pow()field i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping exponentiation.pow_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
left shift<< <<= .shl()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping left shift.shl_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
right shift>> >>= .shr()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
wrapping right shift.shr_wrapped()i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
absolute value.abs()i8 i16 i32 i64 i128
wrapping absolute value.abs_wrapped()i8 i16 i32 i64 i128
doubling.double()field group
squaring.square()field
square root.square_root()field
inverse.square_root()field

Logical Operators

OperationOperandsSupported Types
NOT! .not()bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
AND& &= .and()bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
OR| |= .or()bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
XOR^ ^= .xor()bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128
NAND.nand()bool
NOR.nor()bool
conditional AND&&bool
conditional OR||bool

Relational Operators

Relational operators will always resolve to a boolean bool value.

OperationOperandsSupported Types
equal== .eq()bool, group, field, integers, addresses, structs, records
not-equal!= .neq()bool, group, field, integers, addresses, structs, records
less than< .lt()field, scalar, integers
less than or equal<= .lte()field, scalar, integers
greater than> .gt()field, scalar, integers
greater than or equal>= .gte()field, scalar, integers

Operator Precedence

Operators will prioritize evaluation according to:

OperatorAssociativity
! -(unary)
**right to left
* /left to right
+ -(binary)left to right
<< >>left to right
&left to right
|left to right
^left to right
< > <= >=
== !=left to right
&&left to right
||left to right
= += -= *= /= %= **= <<= >>= &= |= ^=

Parentheses

To prioritize a different evaluation, use parentheses () around the expression.

let result = (a + 1u8) * 2u8;

(a + 1u8) will be evaluated before multiplying by two * 2u8.