In this chapter, we'll explore operators and expressions in the Leo programming language. Leo offers a long list of operators for performing arithmetic operations, logical comparisons, bitwise manipulations, and cryptographic functions.
Basic Concepts
Operators in Leo compute a value based on one or more expressions. Leo defaults to checked arithmetic. This means that operations like addition, subtraction, and multiplication will throw an error if an overflow or division by zero is detected.
Let's look at a simple example:
leta:u8=1u8+1u8;// a is equal to 2a+=1u8;// a is now equal to 3a=a.add(1u8);// a is now equal to 4
This example demonstrates three different ways to perform addition in Leo:
Using the + operator
Using the += compound assignment operator
Using the .add() method
Operator Precedence
When an expression contains multiple operators, Leo follows a strict order of evaluation based on operator precedence. Here's the complete precedence table, from highest (evaluated first) to lowest:
Operator
Associativity
!-(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
=+=-=*=/=%=**=<<=>>=&=|=^=
Using Parentheses for Explicit Ordering
To override the default precedence, you can use parentheses to explicitly control the order of evaluation:
In this example, (a + 1u8) will be evaluated first, then the result will be multiplied by 2u8.
Context-dependent Expressions
Leo supports special expressions that provide information about the Aleo blockchain and the current transaction.
self.caller
The self.caller expression returns the address of the account or program that invoked the current transition:
self.signer
The self.signer expression returns the address of the account that invoked the top-level transition - the account that signed the transaction:
block.height
The block.height expression returns the current block height. Note that this can only be used in an async function:
Core Functions
Leo provides several built-in functions for assertions and cryptographic operations.
Assertions
The assert and assert_eq functions verify conditions and halt program execution if they fail:
Hash Functions
Leo supports multiple hashing algorithms, each with different input sizes and output types:
Available hashing algorithms include:
BHP256, BHP512, BHP768, BHP1024
Pedersen64, Pedersen128
Poseidon2, Poseidon4, Poseidon8
Keccak256, Keccak384, Keccak512
SHA3_256, SHA3_384, SHA3_512
Commitment Functions
Commitment schemes allow you to commit to a value while keeping it hidden, then reveal it later:
Random Number Generation
The ChaCha algorithm provides cryptographically secure random number generation. These functions can only be used in async functions:
Standard Operators
Let's explore the standard operators available in Leo in more detail.
Arithmetic Operators
Addition (+, add, add_wrapped)
Subtraction (-, sub, sub_wrapped)
Multiplication (*, mul, mul_wrapped)
Division (/, div, div_wrapped)
Exponentiation (**, pow, pow_wrapped)
Comparison Operators
Method syntax is also available for these comparisons:
Logical Operators
Method syntax:
Bitwise Operators
Method syntax:
Wrapped versions are also available for shift operations:
Other Mathematical Operators
Ternary Operator
The ternary (conditional) operator allows for concise conditional expressions:
Signature Verification
Special Operators and Constants
Group Generator
The group::GEN constant provides access to the generator point of the elliptic curve group:
Double Operation
The double method performs doubling operation on field and group elements:
Inverse Operations
The multiplicative inverse can be computed for field elements:
Best Practices for Operators
When working with operators in Leo, keep these best practices in mind:
Use checked operations by default for better safety, unless you specifically need wrapping behavior.
Be careful with type conversions when working with different numeric types.
Consider overflow/underflow risks when performing operations near the bounds of a type's range.
Use parentheses liberally to make your code's precedence explicit, especially in complex expressions.
Take advantage of method syntax when it makes your code more readable.
Be mindful of the differences between rem and mod operations, especially when working with negative numbers.
Use cryptographic operations appropriately based on your security and performance requirements.
Common Patterns and Examples
Safely Incrementing a Counter
Computing an Average
Checking Permissions
Creating a Simple Hash-based Commitment
Type-Specific Operator Behavior
Different types in Leo may have different behaviors with the same operators. Here's a quick summary:
program test.aleo {
transition matches() {
assert(true); // Continues execution
assert_eq(1u8, 1u8); // Continues execution
// assert(false); // Would halt execution
// assert_eq(1u8, 2u8); // Would halt execution
}
}
let a: scalar = BHP256::hash_to_scalar(1u8);
let b: address = Pedersen64::hash_to_address(1u128);
let c: group = Poseidon2::hash_to_group(1field);
let salt: scalar = ChaCha::rand_scalar();
let a: group = BHP256::commit_to_group(1u8, salt);
let b: address = Pedersen64::commit_to_address(1u128, salt);
let a: group = ChaCha::rand_group();
let b: u32 = ChaCha::rand_u32();
// Regular checked addition
let a: u8 = 1u8 + 2u8; // a = 3
// Method syntax for addition
let b: u8 = a.add(4u8); // b = 7
// Wrapping addition (does not check for overflow)
let c: u8 = 255u8.add_wrapped(1u8); // c = 0 (wraps around)
// Regular checked subtraction
let a: u8 = 5u8 - 2u8; // a = 3
// Method syntax
let b: u8 = a.sub(1u8); // b = 2
// Wrapping subtraction
let c: u8 = 0u8.sub_wrapped(1u8); // c = 255 (wraps around)
// Regular checked multiplication
let a: u8 = 3u8 * 2u8; // a = 6
// Method syntax
let b: u8 = a.mul(2u8); // b = 12
// Wrapping multiplication
let c: u8 = 128u8.mul_wrapped(2u8); // c = 0 (wraps around)
// Regular checked division
let a: u8 = 8u8 / 2u8; // a = 4
// Method syntax
let b: u8 = a.div(2u8); // b = 2
// Wrapping division
// For signed integers, handles special cases like MIN_VALUE / -1
let c: i8 = (-128i8).div_wrapped(-1i8); // c = -128 (would overflow in checked division)
// Regular checked exponentiation
let a: u8 = 2u8 ** 3u8; // a = 8
// Method syntax
let b: u8 = a.pow(2u8); // b = 64
// Wrapping exponentiation
let c: u8 = 16u8.pow_wrapped(2u8); // c = 0 (wraps around)
let a: u8 = 5u8;
let b: u8 = 10u8;
let eq: bool = a == b; // false
let neq: bool = a != b; // true
let lt: bool = a < b; // true
let lte: bool = a <= b; // true
let gt: bool = a > b; // false
let gte: bool = a >= b; // false
let eq: bool = a.eq(b); // false
let neq: bool = a.neq(b); // true
let lt: bool = a.lt(b); // true
let lte: bool = a.lte(b); // true
let gt: bool = a.gt(b); // false
let gte: bool = a.gte(b); // false
let a: bool = true;
let b: bool = false;
let and: bool = a && b; // false
let or: bool = a || b; // true
let not: bool = !a; // false
let and: bool = a.and(b); // false
let or: bool = a.or(b); // true
let not: bool = a.not(); // false
let nand: bool = a.nand(b); // true (!(a && b))
let nor: bool = a.nor(b); // false (!(a || b))
let xor: bool = a.xor(b); // true (a != b)
let a: u8 = 0b1100u8; // 12 in decimal
let b: u8 = 0b1010u8; // 10 in decimal
let and: u8 = a & b; // 0b1000 (8 in decimal)
let or: u8 = a | b; // 0b1110 (14 in decimal)
let xor: u8 = a ^ b; // 0b0110 (6 in decimal)
let not: u8 = !a; // 0b11110011 (243 in decimal)
let shl: u8 = a << 1u8; // 0b11000 (24 in decimal)
let shr: u8 = a >> 1u8; // 0b0110 (6 in decimal)
let and: u8 = a.and(b); // 0b1000 (8)
let or: u8 = a.or(b); // 0b1110 (14)
let xor: u8 = a.xor(b); // 0b0110 (6)
let not: u8 = a.not(); // 0b11110011 (243)
let shl: u8 = a.shl(1u8); // 0b11000 (24)
let shr: u8 = a.shr(1u8); // 0b0110 (6)
let shl_wrapped: u8 = 128u8.shl_wrapped(1u8); // 0 (wraps around)
let shr_wrapped: u8 = 1u8.shr_wrapped(1u8); // 0
// Remainder (uses truncated division rules)
let rem: u8 = 7u8 % 3u8; // 1
let rem_method: u8 = 7u8.rem(3u8); // 1
// Modulo (always follows mathematical definition of modulo)
let mod_value: u8 = 7u8.mod(3u8); // 1 (same as rem for positive numbers)
// Absolute value
let abs_value: i8 = (-5i8).abs(); // 5i8
// Square
let square: field = 5field.square(); // 25field
// Square root
let sqrt: field = 25field.square_root(); // 5field
let condition: bool = true;
let value: u8 = condition ? 1u8 : 2u8; // value = 1u8
// Verify that a signature was created by a specific address for a specific message
let is_valid: bool = signature::verify(sig, signer_address, message);
// Method syntax
let also_valid: bool = sig.verify(signer_address, message);
let g: group = group::GEN; // The group generator
let a: field = 3field;
let doubled_a: field = a.double(); // 6field
let p: group = 2group;
let doubled_p: group = p.double(); // Point doubling on the elliptic curve
let a: field = 2field;
let a_inv: field = a.inv(); // 1/2 in the field
// Safe increment with overflow checking
function increment(counter: u32) -> u32 {
return counter + 1u32; // Will halt if counter is at maximum
}
// Wrapping increment for cases where overflow is acceptable
function wrapping_increment(counter: u32) -> u32 {
return counter.add_wrapped(1u32); // Will wrap to 0 if counter is at maximum
}
function average(a: u32, b: u32) -> u32 {
// Avoid overflow by adding in a different order
return a / 2u32 + b / 2u32 + (a % 2u32 + b % 2u32) / 2u32;
}
function has_permission(user: address, owner: address, admin: address) -> bool {
return user == owner || user == admin;
}
function create_commitment(value: u64, salt: scalar) -> field {
return BHP256::hash_to_field((value, salt));
}