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 + 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:

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:

  1. Use checked operations by default for better safety, unless you specifically need wrapping behavior.

  2. Be careful with type conversions when working with different numeric types.

  3. Consider overflow/underflow risks when performing operations near the bounds of a type's range.

  4. Use parentheses liberally to make your code's precedence explicit, especially in complex expressions.

  5. Take advantage of method syntax when it makes your code more readable.

  6. Be mindful of the differences between rem and mod operations, especially when working with negative numbers.

  7. 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