Skip to main content

Data Types

Primitive Types

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 sender: address = aleo1ezamst4pjgj9zfxqq0fwfj8a4cjuqndmasgata3hggzqygggnyfq6kmyd4;
let receiver = aleo129nrpl0dxh4evdsan3f4lyhz5pdgp6klrn5atp37ejlavswx5czsk0j5dj;

Booleans

Leo supports the traditional true or false boolean values.

let b: bool = false;
let a = false;

Integers

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

let bint: u8 = 1u8;

Underscores _ can be used to separate digits in integer literals.

let n: u64 = 1_000_000u64;
info

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

info

Leo does not assume a default integer type. Every integer must either have an explicit type annotation or a type that can be inferred by the compiler.

let ac: u8 = 2u8; // explicit type
let bc: u16 = ac as u16; // type casting

Field Elements

Leo supports the field type for elements of the base field of the elliptic curve. These are unsigned integers less than the modulus of the base field. The following are the smallest and largest field elements.

let af: field = 0field;
let bf = 8444461749428370424248824938781546531375899335154063827935233455917409239040field;
let cf: field = 0;

Group Elements

The set of affine points on the elliptic curve forms a group. The curve is a Twisted Edwards curve with a = -1 and d = 3021. Leo supports a subgroup of the group, generated by a generator point, as a primitive data type. A group element is denoted by the x-coordinate of its point; for example, 2group means the point (2, 5553594316923449299484601589326170487897520766531075014687114064346375156608).

let ag: group = 0group; // the point with 0 x-coordinate, (0, 1)
let bg = 1540945439182663264862696551825005342995406165131907382295858612069623286213group; // the generator point
let cg: group = 0;

The aforementioned generator point can be obtained via a constant associated to the group type.

let g: group = group::GEN; // the group generator

Scalar Elements

Leo supports the scalar type for elements of the scalar field defined by the elliptic curve subgroup. These are unsigned integers less than the modulus of the scalar field. The following are the smallest and largest scalars.

let asc: scalar = 0scalar;
let bsc = 2111115437357092606062206234695386632838870926408408195193685246394721360382scalar;
let csc: scalar = 0;

Signatures

Aleo uses the Schnorr signature scheme to sign messages with an Aleo private key. Signatures are a native type in Leo, and can be declared with the keyword signature. Signatures can be verified in Leo using the signature::verify or s.verify operators.

struct foo {
a: u8,
b: scalar
}

program test.aleo {
fn verify_field(s: signature, a: address, v: field) {
let first: bool = signature::verify(s, a, v);
let second: bool = s.verify(a, v);
assert_eq(first, second);
}

fn verify_foo(s: signature, a: address, v: foo) {
let first: bool = signature::verify(s, a, v);
let second: bool = s.verify(a, v);
assert_eq(first, second);
}

@noupgrade
constructor() {}
}

Signature literals can also be used directly in source. They follow a fixed Bech32m grammar:

  • The literal begins with the prefix sign followed by the Bech32m separator 1 — the first five characters of every signature literal are sign1.
  • After the separator, exactly 211 characters drawn from the Bech32m alphabet (qpzry9x8gf2tvdw0s3jn54khce6mua7l — lowercase letters and digits, excluding b, i, o, and 1).
  • The total length is therefore 216 characters. No surrounding quotes, no internal whitespace.
  • The trailing six characters of the body form a Bech32m checksum. The compiler validates the encoding and checksum at parse time and rejects any malformed literal.

Most users do not type signature literals by hand: the value is produced by signing a message with an Aleo private key (for example, with leo account sign or any external tool that produces Aleo signatures) and then pasted into source. Example:

let sig: signature = sign195m229jvzr0wmnshj6f8gwplhkrkhjumgjmad553r997u7pjfgpfz4j2w0c9lp53mcqqdsmut2g3a2zuvgst85w38hv273mwjec3sqjsv9w6uglcy58gjh7x3l55z68zsf24kx7a73ctp8x8klhuw7l2p4s3aq8um5jp304js7qcnwdqj56q5r5088tyvxsgektun0rnmvtsuxpe6sj;

Identifiers

The identifier type represents a SnarkVM identifier name resolved at runtime. It is often used with dynamic calls to specify which program to call at runtime without knowing it at compile time.

An identifier literal uses single-quote syntax:

let target: identifier = 'my_program';

Literal grammar

The contents between the single quotes must satisfy snarkVM's identifier rules:

  • Character set: ASCII letters (AZ, az), ASCII digits (09), and underscore (_). No other characters are accepted. The Leo lexer rejects anything else at parse time.
  • Leading character: must be an ASCII letter; identifiers cannot start with a digit or an underscore.
  • Length: 1 to 31 characters, inclusive. The 31-byte limit comes from how snarkVM packs identifier strings into a single field element and is enforced when the bytecode is generated.
  • Reserved names: the following type-name strings cannot be used as identifier literals — address, boolean, field, group, scalar, signature, string, i8, i16, i32, i64, i128, u8, u16, u32, u64, u128. (Leo keywords such as program, fn, or let are legal inside an identifier literal — only snarkVM literal-type names are reserved at this layer.)

For example, 'aleo', 'my_program', 'foo_v2', and 'X' are valid; '1foo', 'a-b', '_x', 'field', and an identifier longer than 31 characters are not.

The same grammar applies to identifier literals used in dynamic-call syntax (Interface@(target, 'aleo')::method(...)) and intrinsic arguments (e.g. _dynamic_call::[...](prog, 'aleo', 'foo', ...)).

Composite Types

Arrays

Leo supports static arrays. Array types are declared as [type; length]. Elements can only be primitive data types, structs, or nested arrays.

// Initialize a boolean array of length 4
let arr: [bool; 4] = [true, false, true, false];

// Empty array
let empty: [u32; 0] = [];

// Nested array
let nested: [[bool; 2]; 2] = [[true, false], [true, false]];

To initialize every slot of a fixed-size array with the same value, use the repeat syntax [expression; count]. Both the expression and the count must be evaluatable at compile time, and the resulting type is [T; count] where T is the type of the expression.

// Repeat a single value to fill a fixed-size array.
let zeros: [u32; 8] = [0u32; 8];
let trues: [bool; 4] = [true; 4];

// The repeat syntax also nests:
let grid: [[u8; 4]; 3] = [[0u8; 4]; 3];

Structs and records can also contain arrays as fields.

struct Bar {
data: [u8; 8],
}

program array_structs.aleo {
record Foo {
owner: address,
data: [u8; 8],
}

fn build() -> Bar {
// Array of structs
let arr_of_structs: [Bar; 2] = [
Bar { data: [1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8] },
Bar { data: [2u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8] },
];
return arr_of_structs[0u8];
}

@noupgrade
constructor() {}
}

Arrays only support constant accesses. The accessor expression must be a constant expression (known at compile-time).

fn array_access(a: [Bar; 8]) -> [u8; 8] {
return a[0u8].data;
}

Arrays can be stored as a mapping input/output, and iterated over using a loop.

program array_mapping.aleo {
// Declare a mapping that contains array values
mapping data: address => [bool; 8];

// Iterate over an array using a for loop and sum the values within
fn sum_with_loop(a: [u64; 4]) -> u64 {
let sum: u64 = 0u64;
for i: u8 in 0u8..4u8 {
sum += a[i];
}
return sum;
}

@noupgrade
constructor() {}
}

Tuples

Leo supports tuples. Tuple types are declared as (type1, type2, ...) and cannot be empty.

Tuples can contain primitive data types, structs, arrays, or nested tuples. Structs and records can also contain tuples.

// Initialize a tuple of mixed types
let tup: (u8, u8, bool) = (1u8, 1u8, true);

// Nested tuple
let nestedt: [[bool; 2]; 2] = [[true, false], [true, false]];

Tuples cannot appear as fields of a struct or record — only primitives, arrays, structs, and other records may.

Tuples only support constant access with a dot . and a constant integer.

fn tuple_access(fa: u8, ba: u8) -> u8 {
let a: (u8, u8) = (fa, ba);
let result: u8 = a.0 + a.1;
return result;
}

Structs

Struct types are declared and constructed with a familiar syntax.

Structs defined within a program can be referenced by their name. Structs defined in other programs must be referenced using the fully qualified form program_name.aleo::StructName.

struct S {
x: field,
y: u32,
}

program test.aleo {
fn foo(y: u32) -> S {
let s: S = S {
x: 172field,
y,
};
return s;
}

@noupgrade
constructor() {}
}

Structs defined in external programs can be referenced and constructed using their fully qualified name:

let s: external_program.aleo::S2 = external_program.aleo::S2 {
x: 1field,
y: 2u32,
};

Leo supports const generics for struct types:

struct Matrix::[N: u32, M: u32] {
data: [field; N * M],
}

program matrix_demo.aleo {
fn build() -> Matrix::[2u32, 2u32] {
// Usage
let m = Matrix::[2u32, 2u32] { data: [0field, 1field, 2field, 3field] };
return m;
}

@noupgrade
constructor() {}
}

Acceptable types for const generic parameters include integer types, bool, scalar, group, field, and address. Const generic structs may be declared and used within a program and its submodules, and can also be imported from external programs and libraries via fully qualified paths. The same applies to const-generic helper functions: a generic helper declared in a library or in another program's submodule can be invoked across packages with a fully qualified call. The compiler monomorphizes each instantiation at the call site. See Generic Library Functions for the library case.

For example, a library declaring a const-generic struct and helper:

// A const-generic pair, parameterised by an integer tag `N`.
struct Pair::[N: u32] {
first: u32,
second: u32,
}

// A const-generic helper that scales `x` by the type-level constant `N`.
fn scale::[N: u32](x: u32) -> u32 {
return x * N;
}

can be consumed from another package by qualifying the path with the library name:

// Construct a const-generic struct defined in another package.
fn build() -> pair_lib::Pair::[3u32] {
return pair_lib::Pair::[3u32] { first: 1u32, second: 2u32 };
}

// Call a const-generic helper defined in another package.
fn triple(x: u32) -> u32 {
return pair_lib::scale::[3u32](x);
}

Records

A record data type is the method of encoding private state on Aleo. Records are declared as record {name} {}. A record name must not contain the keyword aleo, and must not be a prefix of any other record name.

Records contain component declarations {visibility} {name}: {type},. Names of record components must not contain the keyword aleo. The visibility qualifier may be specified as constant, public, or private. If no qualifier is provided, Leo defaults to private.

Record data structures must always contain a component named owner of type address, as shown below. When passing a record as input to a program function, the _nonce: group and _version: u8 components are also required but do not need to be declared in the Leo program. They are inserted automatically by the compiler.

record Token {
// The token owner.
owner: address,
// The token amount.
amount: u64,
}

Option Types

As of v3.3.0, Leo supports first-class option types using the T? syntax, where T is any of the types previously mentioned, excluding record, address, signature, and tuple. A value of type T? can be initialized into two states: either a value of type T, or none:

let w: u8? = 42u8;
let x: u8? = none;

A value of type T? can be converted to type T by calling the .unwrap() method on the value. Note that if the value being unwrapped is none, then the program will fail to execute. To unwrap the value with a fallback for this case, call the .unwrap_or() method:

let yo = w.unwrap();        // Returns 42u8
let zo = x.unwrap_or(99u8); // Returns 99u8

Option types can also be stored in arrays and structs:

// Struct of options
struct Point {
x: u32?,
y: u32?
}

program options_demo.aleo {
fn demo() -> u16 {
// Array of options
let arr: [u16?; 2] = [1u16, none];
let first_val = arr[0].unwrap(); // Returns 1u16
let second_val = arr[1].unwrap_or(0u16); // Returns 0u16

// Structs have option variant as well
let p: Point? = none;
let p_val = p.unwrap_or(Point { x: 0u32, y: none }); // Returns default

return first_val + second_val;
}

@noupgrade
constructor() {}
}

Type Casting

Leo provides an as operator that converts a value of one primitive type into another:

expression as TargetType
// Widening between integer types — always succeeds.
let small_u8: u8 = 200u8;
let widened: u64 = small_u8 as u64;

// Integer to field — always succeeds.
let n_u32: u32 = 5u32;
let n_field: field = n_u32 as field;

// Address to field — always succeeds.
let addr_a: address = aleo1ezamst4pjgj9zfxqq0fwfj8a4cjuqndmasgata3hggzqygggnyfq6kmyd4;
let addr_field: field = addr_a as field;

Castable types

The compiler accepts casts whose source and target are both drawn from this set:

  • address
  • bool
  • field
  • group
  • scalar
  • All integer types (i8, i16, i32, i64, i128, u8, u16, u32, u64, u128)
  • identifier (used in dynamic dispatch — see Interfaces)

The following are never castable:

  • signature
  • struct, record, tuple, mapping, and array types
  • Optional (T?) — unwrap first via .unwrap() or .unwrap_or(...)

For converting a static record into dyn record and back, see Dynamic Records — that is a separate cast form with its own rules.

Runtime semantics

Leo's as operator lowers to the AVM cast instruction, which is checked: the cast halts at runtime if the source value cannot be represented in the destination type. Common cases that may halt:

  • Narrowing integer casts (u64 as u8, i32 as u8, etc.): halt if the value does not fit in the destination type, including signed-to-unsigned conversions of negative values.
  • fieldaddress: halts unless the field encodes a valid Aleo address (a curve point).
  • fieldgroup: halts unless the field is the x-coordinate of a valid curve point.
  • field → integer types: halts if the field value does not fit.
  • fieldscalar: halts unless the field value is below the scalar field order.
  • group → integer, groupscalar, address → integer, addressscalar, scalar → integer: take the underlying field/x-coordinate and halt if it does not fit the destination type.
  • integergroup, scalargroup, integeraddress, scalaraddress: try to interpret the source as the x-coordinate of a curve point; halt if no valid point exists.

Widening integer casts (u8 as u64, i32 as i128, etc.), integerfield, scalarfield, groupaddress (bijective), groupfield, addressfield, bool → anything, and casts between primitive types where the destination strictly contains the source value range never halt.

The exact runtime rules are defined by the AVM cast instruction (see the Aleo Instructions reference). Leo does not currently expose AVM's cast.lossy (truncating) variant; when in doubt, prefer routing through field (the universally-receiving type) before narrowing.

Type Inference

As of v2.7.0, Leo supports type inference. The Leo compiler is able to infer the types of declared variables and expressions as long as the type can be unambiguously determined from the surrounding context.

If the compiler cannot infer the type, you must provide an explicit type annotation.

Here are some examples:

let ainf: u8 = 2u8; // explicit type - allowed
let binf = 2u8; // type inference - allowed
let cinf: u8 = 2; // type inference - allowed

The exception — bindings with no annotation and an ambiguously-typed literal:

let d = 2; // ambiguous type - not allowed

Type inference also applies to members within a struct:

struct Foo {
x: u8
}

program type_inference_demo.aleo {
fn build() -> Foo {
let f = Foo {
x: 5, // inferred to be a `u8`
};
return f;
}

@noupgrade
constructor() {}
}