3.7 Operators
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:
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
This example demonstrates three different ways to perform addition in Leo:
Using the
+
operatorUsing the
+=
compound assignment operatorUsing 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:
!
-
(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:
let result = (a + 1u8) * 2u8;
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:
program test.aleo {
transition matches(addr: address) -> bool {
return self.caller == addr;
}
}
self.signer
The self.signer
expression returns the address of the account that invoked the top-level transition - the account that signed the transaction:
program test.aleo {
transition matches(addr: address) -> bool {
return self.signer == addr;
}
}
block.height
The block.height
expression returns the current block height. Note that this can only be used in an async function:
program test.aleo {
async transition matches(height: u32) -> Future {
return check_block_height(height);
}
async function check_block_height(height: u32) {
assert_eq(height, block.height);
}
}
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:
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
}
}
Hash Functions
Leo supports multiple hashing algorithms, each with different input sizes and output types:
let a: scalar = BHP256::hash_to_scalar(1u8);
let b: address = Pedersen64::hash_to_address(1u128);
let c: group = Poseidon2::hash_to_group(1field);
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:
let salt: scalar = ChaCha::rand_scalar();
let a: group = BHP256::commit_to_group(1u8, salt);
let b: address = Pedersen64::commit_to_address(1u128, salt);
Random Number Generation
The ChaCha algorithm provides cryptographically secure random number generation. These functions can only be used in async functions:
let a: group = ChaCha::rand_group();
let b: u32 = ChaCha::rand_u32();
Standard Operators
Let's explore the standard operators available in Leo in more detail.
Arithmetic Operators
Addition (+
, add
, add_wrapped
)
// 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)
Subtraction (-
, sub
, sub_wrapped
)
// 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)
Multiplication (*
, mul
, mul_wrapped
)
// 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)
Division (/
, div
, div_wrapped
)
// 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)
Exponentiation (**
, pow
, pow_wrapped
)
// 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)
Comparison Operators
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
Method syntax is also available for these comparisons:
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
Logical Operators
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
Method syntax:
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)
Bitwise Operators
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)
Method syntax:
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)
Wrapped versions are also available for shift operations:
let shl_wrapped: u8 = 128u8.shl_wrapped(1u8); // 0 (wraps around)
let shr_wrapped: u8 = 1u8.shr_wrapped(1u8); // 0
Other Mathematical Operators
// 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
Ternary Operator
The ternary (conditional) operator allows for concise conditional expressions:
let condition: bool = true;
let value: u8 = condition ? 1u8 : 2u8; // value = 1u8
Signature Verification
// 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);
Special Operators and Constants
Group Generator
The group::GEN
constant provides access to the generator point of the elliptic curve group:
let g: group = group::GEN; // The group generator
Double Operation
The double
method performs doubling operation on field and group elements:
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
Inverse Operations
The multiplicative inverse can be computed for field elements:
let a: field = 2field;
let a_inv: field = a.inv(); // 1/2 in the field
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
andmod
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
// 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
}
Computing an Average
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;
}
Checking Permissions
function has_permission(user: address, owner: address, admin: address) -> bool {
return user == owner || user == admin;
}
Creating a Simple Hash-based Commitment
function create_commitment(value: u64, salt: scalar) -> field {
return BHP256::hash_to_field((value, salt));
}
Type-Specific Operator Behavior
Different types in Leo may have different behaviors with the same operators. Here's a quick summary:
Integer Types (i8, i16, i32, i64, i128, u8, u16, u32, u64, u128)
Support full arithmetic operations
Checked operations will halt on overflow/underflow
Wrapping operations available for all arithmetic operations
Support all bitwise operations
Field Type
Supports addition, subtraction, multiplication, division, square, square_root, inverse
Does not support bitwise operations
No overflow concerns (the field is very large)
Group Type
Supports addition and scalar multiplication (for elliptic curve points)
Supports negation and doubling
Special operation: scalar multiplication with a scalar type
Boolean Type
Supports logical operations: and, or, not, xor, nand, nor
Can be used in ternary operations as the condition
Last updated