Cairo by Example

Cairo is a modern programming language that lets you write ZK-provable programs without requiring a deep understanding of the underlying ZK concepts. Its Rust-inspired design that makes it easy to build scalable dApps with the power of validity proofs.

Cairo by Example (CBE) is a collection of runnable examples that illustrate various Cairo concepts and standard libraries. To get even more out of these examples, don't forget to install Cairo locally and check out The Cairo Book. Additionally for the curious, you can also check out the source code for this site.

Now let's begin!

  • Hello World - Start with a traditional Hello World program.

  • Primitives - Learn about signed integers, unsigned integers and other primitives.

  • Custom Types - struct and enum.

  • Variable Bindings - mutable bindings, scope, shadowing.

  • Types - Learn about changing and defining types.

  • Conversion - Convert between different types, such as strings, integers, and floats.

  • Expressions - Learn about Expressions & how to use them.

  • Flow of Control - if/else, for, and others.

  • Functions - Learn about Methods, Closures and Higher Order Functions.

  • Modules - Organize code using modules

  • Crates - A crate is a compilation unit in Cairo. Learn how it is structured.

  • Scarb - Go through some basic features of the official Cairo package management tool and build system.

  • Attributes - An attribute is metadata applied to some module, crate or item.

  • Generics - Learn about writing a function or data type which can work for multiple types of arguments.

  • Scoping rules - Scopes play an important part in ownership, borrowing, and lifetimes.

  • Traits - A trait is a collection of methods.

  • Error handling - Learn the Cairo way of handling failures.

  • Core library types - Learn about some custom types provided by core library.

  • Testing - All sorts of testing in Cairo.

  • Meta - Documentation, Benchmarking.

Hello World

This is the source code of the traditional Hello World program.

// This is a comment, and is ignored by the compiler.
// You can test this code by clicking the "Run" button over there ->
// or if you prefer to use your keyboard, you can use the "Ctrl + Enter"
// shortcut.

// This code is editable, feel free to hack it!
// You can always return to the original code by clicking the "Reset" button ->

// This is the main function.
fn main() {
    // Statements here are executed when the compiled binary is called.

    // Print text to the console.
    println!("Hello World!");
}

println! is a macro that prints text to the console.

A compiled program can be generated using the Cairo compiler through Scarb: scarb build.

$ scarb build

scarb build will produce a hello binary that can be executed. scarb cairo-run will run the program.

$ scarb cairo-run

Activity

Click 'Run' above to see the expected output. Next, add a new line with a second println! macro so that the output shows:

Hello World!
I'm a Caironaute!

Comments

Any program requires comments, and Cairo supports a single few different varieties:

  • Regular comments which are ignored by the compiler:
    • // Line comments which go to the end of the line.
  • Doc comments which are parsed into HTML library [documentation][docs]:
    • /// Generate library docs for the following item.
    • //! Generate library docs for the enclosing item.
//! This is an example of a module-level doc comment.

/// This is an example of a item-level doc comment.
fn main() { // This is an example of a line comment.
// There are two slashes at the beginning of the line.
// And nothing written after these will be read by the compiler.

// println!("Hello, world!");

// Run it. See? Now try deleting the two slashes, and run it again
}

Formatted print

Printing is handled by a series of macros defined in core::fmt some of which are:

  • format!: write formatted text to ByteArray
  • print!: same as format! but the text is printed to the console (stdout).
  • println!: same as print! but a newline is appended.

All parse text in the same fashion. As a plus, Cairo checks formatting correctness at compile time.

//TAG: does_not_run

struct Structure {
    inner: i32,
}

fn main() {
    // In general, the `{}` will be automatically replaced with any
    // arguments. These will be stringified.
    println!("{} days", 31);

    // Positional arguments can be used. Specifying an integer inside `{}`
    // determines which additional argument will be replaced. Arguments start
    // at 0 immediately after the format string.
    let alice: ByteArray = "Alice";
    let bob: ByteArray = "Bob";
    println!("{0}, this is {1}. {1}, this is {0}", alice, bob);

    // Different formatting can be invoked by specifying the format character
    // after a `:`.
    println!("Base 10:               {}", 69420); // 69420
    println!("Base 16 (hexadecimal): {:x}", 69420); // 10f2c

    // Cairo even checks to make sure the correct number of arguments are used.
    let bond: ByteArray = "Bond";
    println!("My name is {0}, {1} {0}", bond);
    // FIXME ^ Add the missing argument: "James"

    // Only types that implement fmt::Display can be formatted with `{}`. User-
// defined types do not implement fmt::Display by default.

    // This will not compile because `Structure` does not implement
// fmt::Display.
// println!("This struct `{}` won't print...", Structure(3));
// TODO ^ Try uncommenting this line
}

core::fmt contains many traits which govern the display of text. The base form of two important ones are listed below:

  • fmt::Debug: Uses the {:?} marker. Format text for debugging purposes.
  • fmt::Display: Uses the {} marker. Format text in a more elegant, user friendly fashion.

Here, we used fmt::Display because the std library provides implementations for these types. To print text for custom types, more steps are required.

Activities

  • Fix the issue in the above code (see FIXME) so that it runs without error.
  • Try uncommenting the line that attempts to format the Structure struct (see TODO)

See also:

std::fmt, macros, struct, traits

Debug

All types which want to use core::fmt formatting traits require an implementation to be printable. Automatic implementations are only provided for types such as in the core library. All others must be manually implemented somehow.

The fmt::Debug trait makes this very straightforward. All types can derive (automatically create) the fmt::Debug implementation. This is not true for fmt::Display which must be manually implemented.

// This structure cannot be printed with `fmt::Display` or
// with `fmt::Debug` by default.
struct UnPrintable {
    value: i32
}

// The `derive` attribute automatically creates the implementation
// required to make this `struct` printable with `fmt::Debug`.
#[derive(Debug)]
struct DebugPrintable {
    value: i32
}

All core library types are automatically printable with {:?} too:

// This structure cannot be printed with `fmt::Display` or
// with `fmt::Debug` by default.
#[derive(Drop)]
struct UnPrintable {
    value: felt252,
}

// The `derive` attribute automatically creates the implementation
// required to make this `struct` printable with `fmt::Debug`.
#[derive(Drop, Debug)]
struct DebugPrintable {
    value: felt252,
}

// Derive the `fmt::Debug` implementation for `Structure`.
#[derive(Drop, Debug)]
struct Structure {
    value: felt252,
}

// Put a `Structure` inside of the structure `Deep`. Make it printable
// also.
#[derive(Drop, Debug)]
struct Deep {
    inner: Structure,
}

fn main() {
    // Printing with `{:?}` is similar to with `{}`.
    println!("{:?} months in a year.", 12);
    let christian: ByteArray = "Christian";
    let slater: ByteArray = "Slater";
    let object: ByteArray = "actor's";
    println!("{1:?} {0:?} is the {2:?} name.", slater, christian, object);

    // `Structure` is printable!
    println!("Now {:?} will print!", Structure { value: 3 });

    // The problem with `derive` is there is no control over how
    // the results look. What if I want this to just show a `7`?
    println!("Now {:?} will print!", Deep { inner: Structure { value: 7 } });
}

So fmt::Debug definitely makes this printable but sacrifices some elegance.

Activities

  • Try removing the Debug derive from one of the structs and see what error you get
  • Add a new field to the Person struct and see how it appears in the debug output
  • Try implementing Display for one of the structs to control its output format

See also:

derive, core::fmt, and struct

Display

fmt::Debug hardly looks compact and clean, so it is often advantageous to customize the output appearance. This is done by manually implementing fmt::Display, which uses the {} print marker. Implementing it looks like this:

    // Import (via `use`) the `fmt` module to make it available.
    use core::fmt;

    // Define a structure for which `fmt::Display` will be implemented. This is
    // a tuple struct named `Structure` that contains an integer.
    #[derive(Drop)]
    struct Structure {
        value: u32,
    }

    // To use the `{}` marker, the trait `fmt::Display` must be implemented
    // manually for the type.
    impl StructureDisplay of fmt::Display<Structure> {
        // This trait requires `fmt` with this exact signature.
        fn fmt(self: @Structure, ref f: fmt::Formatter) -> Result<(), fmt::Error> {
            // Write strictly the first element into the supplied output
            // stream: `f`. Returns `fmt::Result` which indicates whether the
            // operation succeeded or failed. Note that `write!` uses syntax which
            // is very similar to `println!`.
            write!(f, "{}", *self.value)
        }
    }

fmt::Display may be cleaner than fmt::Debug but this presents a problem for the core library. How should ambiguous types be displayed? For example, if the core library implemented a single style for all Array<T>, what style should it be? Would it be either of these two?

  • Array<ContractAddress>: 0x123, 0x456, 0x789 (using hex representation)
  • Array<number>: 1,2,3 (using decimal representation)

No, because there is no ideal style for all types and the core library doesn't presume to dictate one. fmt::Display is not implemented for Array<T> or for any other generic containers. fmt::Debug must then be used for these generic cases.

This is not a problem though because for any new container type which is not generic, fmt::Display can be implemented.

use core::fmt; // Import `fmt`

// A structure holding two numbers. `Debug` will be derived so the results can
// be contrasted with `Display`.
#[derive(Debug, Drop)]
struct MinMax {
    min: i64,
    max: i64,
}

// Implement `Display` for `MinMax`.
impl MinMaxDisplay of fmt::Display<MinMax> {
    fn fmt(self: @MinMax, ref f: fmt::Formatter) -> Result<(), fmt::Error> {
        // Use `self.number` to refer to each positional data point.
        write!(f, "({}, {})", *self.min, *self.max)
    }
}

// Define a structure where the fields are nameable for comparison.
#[derive(Debug, Drop)]
struct Point2D {
    x: i64,
    y: i64,
}

// Similarly, implement `Display` for `Point2D`.
impl Point2DDisplay of fmt::Display<Point2D> {
    fn fmt(self: @Point2D, ref f: fmt::Formatter) -> Result<(), fmt::Error> {
        // Customize so only `x` and `y` are denoted.
        write!(f, "x: {}, y: {}", *self.x, *self.y)
    }
}

fn main() {
    let minmax = MinMax { min: 0, max: 14 };

    println!("Compare structures:");
    println!("Display: {}", minmax);
    println!("Debug: {:?}", minmax);

    let big_range = MinMax { min: -300, max: 300 };
    let small_range = MinMax { min: -3, max: 3 };

    println!("The big range is {} and the small is {}", big_range, small_range);

    let point = Point2D { x: 3, y: 7 };

    println!("Compare points:");
    println!("Display: {}", point);
    // Error. Both `Debug` and `Display` were implemented, but `{:x}`
// requires `fmt::LowerHex` to be implemented. This will not work.
// println!("What does Point2D look like in binary: {:x}?", point);
}

So, fmt::Display has been implemented but fmt::LowerHex has not, and therefore cannot be used. core::fmt has many such traits and each requires its own implementation. This is detailed further in core::fmt.

Activity

After checking the output of the above example, use the Point2D struct as a guide to add a Complex struct to the example. When printed in the same way, the output should be:

Display: 3 + 7i
Debug: Complex { real: 3, imag: 7 }

See also:

derive, core::fmt, macros, struct, trait, and use

Testcase: List

Implementing fmt::Display for a structure where the elements must each be handled sequentially is tricky. The problem is that each write! generates a fmt::Result. Proper handling of this requires dealing with all the results. Cairo provides the ? operator for exactly this purpose.

While the ? operator is available in the language, it does not work inside loops. You can break with the Err value to exit the loop, and use the ? operator on the returned value.

use core::fmt; // Import the `fmt` module.

// Define a structure named `List` containing an `Array`.
#[derive(Drop)]
struct List {
    inner: Array<i32>,
}

impl ListDisplay of fmt::Display<List> {
    fn fmt(self: @List, ref f: fmt::Formatter) -> Result<(), fmt::Error> {
        // Create a span with the array's data.
        let array_span = self.inner.span();

        write!(f, "[")?;

        // Iterate over `v` in `array_span` while enumerating the iteration
        // count in `count`.
        let mut count = 0;
        loop {
            if count >= array_span.len() {
                break Ok(());
            }
            // For every element except the first, add a comma.
            // Use the ? operator to return on errors.
            if count != 0 {
                match write!(f, ", ") {
                    Ok(_) => {},
                    Err(e) => { break Err(e); },
                }
            }
            match write!(f, "{}", *array_span[count]) {
                Ok(_) => {},
                Err(e) => { break Err(e); },
            }
            count += 1;
        }?;

        // Close the opened bracket and return a fmt::Result value.
        write!(f, "]")
    }
}

fn main() {
    let mut arr = ArrayTrait::new();
    arr.append(1);
    arr.append(2);
    arr.append(3);
    let v = List { inner: arr };
    println!("{}", v);
}

Activity

Try changing the program so that the index of each element in the array is also printed. The new output should look like this:

[0: 1, 1: 2, 2: 3]

See also:

for, ref, Result, struct, ?, and Array

Formatting

We've seen that formatting is specified via a format string:

  • format!("{}", foo) -> "3735928559"
  • format!("{:x}", foo) -> "0xdeadbeef"

The same variable (foo) can be formatted differently depending on which argument type is used: x vs unspecified.

This formatting functionality is implemented via traits, and there is one trait for each argument type. The most common formatting trait is Display, which handles cases where the argument type is left unspecified: {} for instance.

use core::fmt::{Formatter, Display};
use core::fmt;

#[derive(Drop)]
struct City {
    name: ByteArray,
    // Latitude
    lat: i32,
    // Longitude
    lon: i32,
}

impl CityDisplay of Display<City> {
    // `f` is a buffer, and this method must write the formatted string into it.
    fn fmt(self: @City, ref f: Formatter) -> Result<(), fmt::Error> {
        let lat_c = if *self.lat >= 0 {
            'N'
        } else {
            'S'
        };
        let lon_c = if *self.lon >= 0 {
            'E'
        } else {
            'W'
        };

        // `write!` is like `format!`, but it will write the formatted string
        // into a buffer (the first argument).
        write!(f, "{}: {}'{} {}'{}", self.name, *self.lat, lat_c, *self.lon, lon_c)
    }
}

#[derive(Debug, Copy, Drop)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

fn main() {
    let dublin = City { name: "Dublin", lat: 53, lon: -6 };
    let oslo = City { name: "Oslo", lat: 59, lon: 10 };
    let vancouver = City { name: "Vancouver", lat: 49, lon: -123 };

    println!("{}", dublin);
    println!("{}", oslo);
    println!("{}", vancouver);

    let colors = array![
        Color { red: 128, green: 255, blue: 90 },
        Color { red: 0, green: 3, blue: 254 },
        Color { red: 0, green: 0, blue: 0 },
    ];

    let mut i = 0;
    loop {
        if i >= colors.len() {
            break;
        }
        // Switch this to use {} once you've added an implementation
        // for fmt::Display.
        println!("{:?}", *colors.at(i));
        i += 1;
    }
}

You can view a full list of formatting traits and their argument types in the core::fmt documentation.

See also:

core::fmt

Primitives

Cairo provides access to a variety of primitives. A sample includes:

Scalar Types

  • Signed integers: i8, i16, i32, i64, i128
  • Unsigned integers: u8, u16, u32, u64, u128, u256
  • Field element: felt252 (unique to Cairo, represents elements in a finite field)
  • bool either true or false
  • The unit type (), whose only possible value is an empty tuple: ()

Note that unlike Rust, Cairo doesn't have floating point numbers or char types due to its focus on zero-knowledge proof computations.

Compound Types

  • Fixed-Size Arrays like [1, 2, 3]
  • Tuples like (1, true)

Variables can always be type annotated. Numbers may additionally be annotated via a suffix. Note that Cairo can also infer types from context.

fn main() {
    // Variables can be type annotated.
    let logical: bool = true;

    // Integer annotations
    let an_integer: u32 = 5_u32; // Regular annotation
    let another_integer = 5_u32; // Suffix annotation

    // felt252 is Cairo's native field element type
    let a_felt = 3; // Default to felt252
    let explicit_felt: felt252 = 3;

    // A type can also be inferred from context
    let mut inferred_type = 12_u64; // Type u64 is inferred
    inferred_type = 4294967296_u64;

    // A mutable variable's value can be changed
    let mut mutable = 12_u32; // Mutable `u32`
    mutable = 21_u32;

    // Error! The type of a variable can't be changed
    // mutable = true;

    // Variables can be overwritten with shadowing
    let mutable = true;

    // Compound types - Array and Tuple //

    // Array signature consists of Type T and length as [T; length]
    let my_array: [u32; 5] = [1_u32, 2_u32, 3_u32, 4_u32, 5_u32];

    // Tuple is a collection of values of different types
    // and is constructed using parentheses ()
    let my_tuple: (u32, u8, bool, felt252) = (5_u32, 1_u8, true, 123);
}

See also:

the Cairo book, mut, inference, and shadowing

Literals and operators

Integers 1, short-strings 'a', ByteArrays "abc", booleans true and the unit type () can be expressed using literals.

Integers can, alternatively, be expressed using hexadecimal, octal or binary notation using these prefixes respectively: 0x, 0o or 0b.

Underscores can be inserted in numeric literals to improve readability, e.g. 1_000 is the same as 1000.

We need to tell the compiler the type of the literals we use. For now, we'll use the u32 suffix to indicate that the literal is an unsigned 32-bit integer, and the i32 suffix to indicate that it's a signed 32-bit integer.

fn main() {
    // Integer addition
    println!("1 + 2 = {}", 1_u32 + 2);

    // Integer subtraction
    println!("1 - 2 = {}", 1_i32 - 2);
    // TODO ^ Try changing `1i32` to `1u32` to see why the type is important

    // Short-circuiting boolean logic
    println!("true AND false is {}", true && false);
    println!("true OR false is {}", true || false);
    println!("NOT true is {}", !true);

    // Bitwise operations
    println!("0011 AND 0101 is {}", 0b0011_u32 & 0b0101);
    println!("0011 OR 0101 is {}", 0b0011_u32 | 0b0101);
    println!("0011 XOR 0101 is {}", 0b0011_u32 ^ 0b0101);

    // Use underscores to improve readability!
    println!("One million is written as {}", 1_000_000_u32);

    // A short-string is the ASCII encoding of the characters.
    println!("Short string `a`: {}", 'a');
}

See also:

Tuples

A tuple is a collection of values of different types. Tuples are constructed using parentheses (), and each tuple itself is a value with type signature (T1, T2, ...), where T1, T2 are the types of its members. Functions can use tuples to return multiple values, as tuples can hold any number of values.

// Tuples can be used as function arguments and as return values.
fn reverse(pair: (felt252, bool)) -> (bool, felt252) {
    // `let` can be used to bind the members of a tuple to variables.
    let (int_param, bool_param) = pair;
    (bool_param, int_param)
}

fn main() {
    // A tuple with a bunch of different types
    let tuple: (u8, ByteArray, i8, bool) = (1, "hello", -1, true);
    // Tuples can be destructured to create bindings.
    let (a, b, c, d) = tuple;
    println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);

    // Tuples can be tuples members.
    let tuple_of_tuples: ((u8, u16, u32), (u64, i8), i16) = ((1, 2, 3), (4, -1), -2);

    // Tuple are printable.
    println!("tuple_of_tuples: {:?}", tuple_of_tuples);

    // But long Tuples (more than 17 elements) cannot be created.
    // let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17);
    // println!("Too long tuple: {:?}", too_long_tuple);
    // TODO ^ Uncomment the above 2 lines to see the compiler error

    // Creating and using a pair tuple.
    let pair = (1, true);
    println!("Pair is {:?}", pair);

    // To create one element tuples, the comma is required to tell them apart
    // from a literal surrounded by parentheses.
    println!("One element tuple: {:?}", (5_u32,));
    println!("Just an integer: {:?}", (5_u32));

    // One-element tuple declaration.
    let one_element_tuple: (u32,) = (5,);
    let (element,) = one_element_tuple;
    println!("element: {}", element);
}

Arrays, Spans, and Fixed-Size Arrays

An Array is a growable collection of objects of the same type T, stored in contiguous memory. Because Cairo's memory is write-once, values stored in an array cannot be modified. The only operation that can be performed on an array is appending elements at the end, or removing elements from the front.

A Span is the [snapshot][snapshot] of an Array - representing a range of elements in the array that can not be appended to. If the array is modified, the associated span will not be affected.

A Fixed-Size Array is an immutable sequence of elements of the same type stored in contiguous memory. Its size and contents are known at compile time, and they're useful to hard-code a sequence of data in your program.

fn main() {
    // Initialize an empty array
    let mut arr = array![];

    // Append elements to the array
    arr.append(1);
    arr.append(2);
    arr.append(3);

    // Indexing starts at 0.
    println!("First element of the array: {}", *arr[0]);
    println!("Second element of the array: {}", *arr[1]);

    // `len()` returns the number of elements in the array.
    println!("Number of elements in the array: {}", arr.len());

    // A span is a snapshot of the array at a certain state.
    let span = arr.span();

    // `pop_front()` removes the first element from the array.
    let _ = arr.pop_front();

    // But the span is not affected by the pop: it has the same state as when the snapshot was
    // taken.
    println!("First element in span: {}", *span[0]);

    // Fixed-size array type
    let xs: [u32; 3] = [1, 2, 3];

    // All elements can be initialized to the same value.
    let ys: [u32; 3] = [0; 3];

    println!("xs: {:?}", xs);
    println!("ys: {:?}", ys);

    // Fixed-size arrays can be converted to spans for operations.
    println!("ys first element: {}", *xs.span()[0]);
}

See also:

Custom Types

Cairo custom data types are formed mainly through the two keywords:

  • struct: define a structure
  • enum: define an enumeration

Constants can also be created via the const keyword.

Structures

Structures ("structs") can be created using the struct keyword using a classic C structs syntax.

#[derive(Drop, Debug)]
struct Person {
    name: ByteArray,
    age: u8,
}

// An empty struct
#[derive(Drop, Debug)]
struct Unit {}

// A struct with two fields
#[derive(Drop)]
struct Point {
    x: u32,
    y: u32,
}

// Structs can be reused as fields of another struct
#[derive(Drop)]
struct Rectangle {
    // A rectangle can be specified by where the top left and bottom right
    // corners are in space.
    top_left: Point,
    bottom_right: Point,
}

fn main() {
    // Create struct with field init shorthand
    let name: ByteArray = "Peter";
    let age = 27;
    let peter = Person { name, age };

    // Print debug struct
    println!("{:?}", peter);

    // Instantiate a `Point`
    let point: Point = Point { x: 5, y: 0 };
    let another_point: Point = Point { x: 10, y: 0 };

    // Access the fields of the point
    println!("point coordinates: ({}, {})", point.x, point.y);

    // Make a new point by using struct update syntax to use the fields of our
    // other one
    let bottom_right = Point { x: 10, ..another_point };

    // `bottom_right.y` will be the same as `another_point.y` because we used that field
    // from `another_point`
    println!("second point: ({}, {})", bottom_right.x, bottom_right.y);

    // Destructure the point using a `let` binding
    let Point { x: left_edge, y: top_edge } = point;

    let _rectangle = Rectangle {
        // struct instantiation is an expression too
        top_left: Point { x: left_edge, y: top_edge }, bottom_right: bottom_right,
    };

    // Instantiate a unit struct
    let _unit = Unit {};
}

Activity

  1. Add a function rect_area which calculates the area of a Rectangle (try using nested destructuring).
  2. Add a function square which takes a Point and a u32 as arguments, and returns a Rectangle with its top left corner on the point, and a width and height corresponding to the u32.

See also

Drop, and destructuring

Enums

The enum keyword allows the creation of a type which may be one of a few different variants. Any variant which is valid as a struct is also valid in an enum.

// Create an `enum` to classify a web event. Note how both
// names and type information together specify the variant:
// `PageLoad != PageUnload` and `KeyPress(felt252) != Paste(ByteArray)`.
// Each is different and independent.
#[derive(Drop)]
enum WebEvent {
    // An `enum` variant may either be `unit-like`,
    PageLoad,
    PageUnload,
    // like tuple structs,
    KeyPress: felt252,
    Paste: ByteArray,
    // or c-like structures.
    Click: Point,
}

#[derive(Drop)]
struct Point {
    x: u64,
    y: u64,
}

// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("page loaded"),
        WebEvent::PageUnload => println!("page unloaded"),
        // Destructure `c` from inside the `enum` variant.
        WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
        WebEvent::Paste(s) => println!("pasted \"{}\".", s),
        // Destructure `Click` into `x` and `y`.
        WebEvent::Click(Point { x, y }) => { println!("clicked at x={}, y={}.", x, y); },
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('x');
    let pasted = WebEvent::Paste("my text");
    let click = WebEvent::Click(Point { x: 20, y: 80 });
    let load = WebEvent::PageLoad;
    let unload = WebEvent::PageUnload;

    inspect(pressed);
    inspect(pasted);
    inspect(click);
    inspect(load);
    inspect(unload);
}

Type aliases

If you use a type alias, you can refer to each enum variant via its alias. This might be useful if the enum's name is too long or too generic, and you want to rename it.


See also:

match, fn, and ByteArray

use

The use declaration can be used so manual scoping isn't needed:

#[derive(Drop)]
enum Stage {
    Beginner,
    Advanced,
}

#[derive(Drop)]
enum Role {
    Student,
    Teacher,
}

// Explicitly `use` each name so they are available without
// manual scoping.
use Stage::{Beginner, Advanced};

fn main() {
    // Equivalent to `Stage::Beginner`.
    let stage = Beginner;

    match stage {
        // Note the lack of scoping because of the explicit `use` above.
        Beginner => println!("Beginners are starting their learning journey!"),
        Advanced => println!("Advanced learners are mastering their subjects..."),
    }
}

See also:

match and use

Testcase: linked-list

A common way to implement a linked-list is via enums. In Cairo, we'll implement a simple linked list that stores its nodes directly in the enum:

use List::{Cons, Nil};

#[derive(Drop)]
enum List {
    // Cons: Tuple struct that wraps an element and a pointer to the next node
    Cons: (u32, Box<List>),
    // Nil: A node that signifies the end of the linked list
    Nil,
}

// Methods can be attached to an enum through a trait implementation
trait ListTrait {
    fn new() -> List;
    fn prepend(self: List, elem: u32) -> List;
    fn len(self: @List) -> u32;
    fn stringify(self: @List) -> ByteArray;
}

impl ListImpl of ListTrait {
    // Create an empty list
    fn new() -> List {
        // `Nil` has type `List`
        Nil
    }

    // Consume a list, and return the same list with a new element at its front
    fn prepend(self: List, elem: u32) -> List {
        // `Cons` also has type List
        Cons((elem, BoxTrait::new(self)))
    }

    // Return the length of the list
    fn len(self: @List) -> u32 {
        // `self` has to be matched, because the behavior of this method
        // depends on the variant of `self`
        match self {
            Cons((_, tail)) => 1 + (tail.as_snapshot().unbox()).len(),
            Nil => 0,
        }
    }

    // Return representation of the list as a string
    fn stringify(self: @List) -> ByteArray {
        match self {
            Cons((
                head, tail,
            )) => {
                let head_str = head.into();
                let tail_str = format!("{}", (tail.as_snapshot().unbox()).stringify());
                format!("{}, {}", head_str, tail_str)
            },
            Nil => { format!("Nil") },
        }
    }
}

fn main() {
    // Create an empty linked list
    let mut list = ListTrait::new();

    // Prepend some elements
    list = list.prepend(1);
    list = list.prepend(2);
    list = list.prepend(3);

    // Show the final state of the list
    println!("linked list has length: {}", list.len());
    println!("{}", list.stringify());
}

See also:

traits and methods

constants

Cairo has a single type of constant which can be declared in any scope including global. It requires explicit type annotation:

  • const: An unchangeable value.
// Globals are declared outside all other scopes.
const LANGUAGE: felt252 = 'Cairo';
const THRESHOLD: u32 = 10;

fn is_big(n: u32) -> bool {
    // Access constant in some function
    n > THRESHOLD
}

fn main() {
    let n = 16;

    // Access constant in the main thread
    println!("This is {}", LANGUAGE);
    println!("The threshold is {}", THRESHOLD);
    let big: ByteArray = "big";
    let small: ByteArray = "small";
    println!("{} is {}", n, if is_big(n) {
        big
    } else {
        small
    });
    // Error! Cannot modify a `const`.
// THRESHOLD = 5;
// FIXME ^ Uncomment this line to see the error
}

Variable Bindings

Cairo provides type safety via static typing. Variable bindings can be type annotated when declared. However, in most cases, the compiler will be able to infer the type of the variable from the context, heavily reducing the annotation burden.

Values (like literals) can be bound to variables, using the let binding.

fn main() {
    let an_integer = 1_u32;
    let a_boolean = true;
    let unit = ();

    // copy `an_integer` into `copied_integer`
    let copied_integer = an_integer;

    println!("An integer: {:?}", copied_integer);
    println!("A boolean: {:?}", a_boolean);
    println!("Meet the unit value: {:?}", unit);

    // The compiler warns about unused variable bindings; these warnings can
    // be silenced by prefixing the variable name with an underscore
    let _unused_variable = 3_u32;

    let noisy_unused_variable = 2_u32;
    // FIXME ^ Prefix with an underscore to suppress the warning
// Please note that warnings may not be shown in a browser
}

Mutability

Variable bindings are immutable by default, but this can be overridden using the mut modifier.

//TAG: does_not_compile

fn main() {
    let _immutable_binding = 1;
    let mut mutable_binding = 1;

    println!("Before mutation: {}", mutable_binding);

    // Ok
    mutable_binding += 1;

    println!("After mutation: {}", mutable_binding);

    // Error! Cannot assign a new value to an immutable variable
    _immutable_binding += 1;
}

The compiler will throw a detailed diagnostic about mutability errors.

Scope and Shadowing

Variable bindings have a scope, and are constrained to live in a block. A block is a collection of statements enclosed by braces {}.

//TAG: does_not_compile

fn main() {
    // This binding lives in the main function
    let long_lived_binding = 1;

    // This is a block, and has a smaller scope than the main function
    {
        // This binding only exists in this block
        let short_lived_binding = 2;

        println!("inner short: {}", short_lived_binding);
    }
    // End of the block

    // Error! `short_lived_binding` doesn't exist in this scope
    println!("outer short: {}", short_lived_binding);
    // FIXME ^ Comment out this line

    println!("outer long: {}", long_lived_binding);
}

Also, variable shadowing is allowed.

fn main() {
    let shadowed_binding = 1;

    {
        println!("before being shadowed: {}", shadowed_binding);

        // This binding *shadows* the outer one
        let shadowed_binding = 456;

        println!("shadowed in inner block: {}", shadowed_binding);
    }
    println!("outside inner block: {}", shadowed_binding);

    // This binding *shadows* the previous binding
    let shadowed_binding = 2;
    println!("shadowed in outer block: {}", shadowed_binding);
}

Freezing

When data is bound by the same name immutably, it also freezes. Frozen data can't be modified until the immutable binding goes out of scope:

//TAG: does_not_compile
fn main() {
    let mut _mutable_integer = 7;

    {
        // Shadowing by immutable `_mutable_integer`
        let _mutable_integer = _mutable_integer;

        // Error! `_mutable_integer` is frozen in this scope
        _mutable_integer = 50;
        // FIXME ^ Comment out this line

        // `_mutable_integer` goes out of scope
    }

    // Ok! `_mutable_integer` is not frozen in this scope
    _mutable_integer = 3;
}

Types

Cairo provides several mechanisms to change or define the type of primitive and user defined types. The following sections cover:

Literals

Numeric literals can be type annotated by adding the type as a suffix. As an example, to specify that the literal 42 should have the type i32, write 42_i32.

The type of unsuffixed numeric literals will depend on how they are used. If no constraint exists, the compiler will use felt252 by default.

fn main() {
    // Suffixed literals, their types are known at initialization
    let _x = 1_u8;
    let _y = 2_u32;
    let _z = 3_i32;

    // Unsuffixed literals, their types depend on how they are used
    let _i = 1;
}

Inference

The type inference engine is pretty smart. It does more than looking at the type of the value expression during an initialization. It also looks at how the variable is used afterwards to infer its type. Here's an advanced example of type inference:

fn main() {
    // Because of the annotation, the compiler knows that `elem` has type u8.
    let elem = 5_u8;

    // Create an empty array.
    let mut array = array![];
    // At this point the compiler doesn't know the exact type of `array`, it
    // just knows that it's an array of something (`Array<_>`).

    // Insert `elem` in the vector.
    array.append(elem);
    // Aha! Now the compiler knows that `array` is an array of `u8`s (`Array<u8>`)
    // TODO ^ Try commenting out the `array.append(elem)` line

    println!("{:?}", array);
}

No type annotation of variables was needed, the compiler is happy and so is the programmer!

Aliasing

The type statement can be used to give a new name to an existing type. Types must have UpperCamelCase names, or the compiler will raise a warning. The exception to this rule are the primitive types: usize, f32, etc.

// `NanoSecond`, `Inch`, and `U64` are new names for `u64`.
type NanoSecond = u64;
type Inch = u64;
type U64 = u64;

fn main() {
    // `NanoSecond` = `Inch` = `U64` = `u64`.
    let nanoseconds: NanoSecond = 5_u64;
    let inches: Inch = 2_u64;

    // Note that type aliases *don't* provide any extra type safety, because
    // aliases are *not* new types
    println!("{} nanoseconds + {} inches = {} unit?", nanoseconds, inches, nanoseconds + inches);
}

The main use of aliases is to reduce boilerplate; for example the SyscallResult<T> type is an alias for the Result<(), Error> type.

See also:

Attributes

Conversion

Cairo addresses conversion between types (i.e., integers, felt252, struct and enum) by the use of traits. The generic conversions will use the and Into and TryInto traits. However there are more specific ones for the more common cases, in particular when converting to and from ByteArrays.

Type Conversion with Into

In Cairo, type conversions are primarily handled through the Into trait. This trait allows you to define how to convert one type into another.

Converting Between Types

The Into trait provides a mechanism for converting between several types. There are numerous implementations of this trait within the core library for conversion of primitive and common types.

For example, we can easily convert a u8 into a u16:

fn main() {
    let my_u8: u8 = 5;
    let _my_u16: u16 = my_u8.into();
}

We can do something similar for defining a conversion for our own type.

#[derive(Drop, Debug)]
struct Number {
    value: u32,
}

impl U32IntoNumber of Into<u32, Number> {
    fn into(self: u32) -> Number {
        Number { value: self }
    }
}

fn main() {
    let int: u32 = 30;
    let num: Number = int.into();
    println!("{:?}", num);
}

TryInto for Fallible Conversions

The TryInto trait is used for conversions that might fail. For example, converting from a larger integer type to a smaller one, or parsing a string into a number.

Unlike Rust which uses Result for fallible operations, Cairo uses Option to represent operations that might fail.

#[derive(Copy, Drop, Debug)]
struct EvenNumber {
    value: u32,
}

impl U32IntoEvenNumber of TryInto<u32, EvenNumber> {
    fn try_into(self: u32) -> Option<EvenNumber> {
        if self % 2 == 0 {
            Option::Some(EvenNumber { value: self })
        } else {
            Option::None
        }
    }
}

fn main() {
    // Direct conversion with TryInto
    let even: Option<EvenNumber> = 8_u32.try_into();
    println!("{:?}", even);

    let odd: Option<EvenNumber> = 5_u32.try_into();
    println!("{:?}", odd);
}

To ByteArray

To convert any type to a ByteArray is as simple as implementing the [fmt::Display] trait for the type, which also allows printing the type as discussed in the section on [print!][print].

use core::fmt;

#[derive(Drop)]
struct Circle {
    radius: i32,
}

impl CircleDisplay of fmt::Display<Circle> {
    fn fmt(self: @Circle, ref f: fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "Circle of radius {}", self.radius)
    }
}

fn main() {
    let circle = Circle { radius: 6 };
    let circle_str: ByteArray = format!("{}", circle);
    println!("{}", circle_str);
}

Expressions

A Cairo program is (mostly) made up of a series of statements:

fn main() {
    // statement
    // statement
    // statement
}

There are a few kinds of statements in Cairo. The most common two are declaring a variable binding, and using a ; with an expression:

fn main() {
    // variable binding
    let x = 5;

    // expression;
    x;
    x + 1;
    15;
}

Blocks are expressions too, so they can be used as values in assignments. The last expression in the block will be assigned to the place expression such as a local variable. However, if the last expression of the block ends with a semicolon, the return value will be ().

fn main() {
    let x = 5_u32;

    let y = {
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // This expression will be assigned to `y`
        x_cube + x_squared + x
    };

    let z = {
        // The semicolon suppresses this expression and `()` is assigned to `z`
        2 * x;
    };

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
}

Flow of Control

An integral part of any programming language are ways to modify control flow: if/else, for, and others. Let's explore how Cairo handles these control flow constructs.

if/else

Branching with if-else is similar to other languages. Unlike many of them, the boolean condition doesn't need to be surrounded by parentheses, and each condition is followed by a block. if-else conditionals are expressions, and, all branches must return the same type.

fn main() {
    let n = 5_i32;

    if n < 0 {
        print!("{} is negative", n);
    } else if n > 0 {
        print!("{} is positive", n);
    } else {
        print!("{} is zero", n);
    }

    let big_n = if n < 10 && n > -10 {
        println!(", and is a small number, increase ten-fold");

        // This expression returns an `i32`.
        10 * n
    } else {
        println!(", and is a big number, halve the number");

        // This expression must return an `i32` as well.
        n / 2
        // TODO ^ Try suppressing this expression with a semicolon.
    };
    //   ^ Don't forget to put a semicolon here! All `let` bindings need it.

    println!("{} -> {}", n, big_n);
}

loop

Cairo provides a loop keyword to indicate an infinite loop.

The break statement can be used to exit a loop at anytime, whereas the continue statement can be used to skip the rest of the iteration and start a new one.

A limitation of loops is that they cannot be labeled nor nested.

fn main() {
    let mut count = 0_u32;

    println!("Let's count until infinity!");

    // Infinite loop
    loop {
        count += 1;

        if count == 3 {
            println!("three");

            // Skip the rest of this iteration
            continue;
        }

        println!("{}", count);

        if count == 5 {
            println!("OK, that's enough");

            // Exit this loop
            break;
        }
    }
}

Returning from loops

One of the uses of a loop is to retry an operation until it succeeds. If the operation returns a value though, you might need to pass it to the rest of the code: put it after the break, and it will be returned by the loop expression.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    assert!(result == 20);
}

while

The while keyword can be used to run a loop while a condition is true.

Let's write the infamous FizzBuzz using a while loop.

fn main() {
    // A counter variable
    let mut n = 1_u8;

    // Loop while `n` is less than 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Increment counter
        n += 1;
    }
}

for loops

for and range

The for in construct can be used to iterate through an Iterator. One of the easiest ways to create an iterator is to use the range notation a..b. This yields values from a (inclusive) to b (exclusive) in steps of one.

Let's write FizzBuzz using for instead of while.

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    for n in 1..101_u8 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

Alternatively, a..=b can be used for a range that is inclusive on both ends. The above can be written as:

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    for n in 1..= 100_u8 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

for and iterators

The for in construct is able to interact with an Iterator in several ways. As discussed in the section on the Iterator trait, by default the for loop will apply the into_iter function to the collection. However, this is not the only means of converting collections into iterators.

into_iter consumes the collection so that on each iteration the exact data is provided. Once the collection has been consumed it is no longer available for reuse as it has been 'moved' within the loop.

  • into_iter - This consumes the collection so that on each iteration the exact data is provided. Once the collection has been consumed it is no longer available for reuse as it has been 'moved' within the loop.
//TAG: does_not_compile
fn main() {
    let names: Array<ByteArray> = array!["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        if name == "Ferris" {
            println!("There is a caironaute among us!");
        } else {
            println!("Hello {}", name);
        }
    }

    println!("names: {:?}", names);
    // FIXME ^ Comment out this line
}

Note: The syntax for elem in collection is equivalent to for elem in collection.into_iter() and requires the IntoIter trait to be implemented for the collection.

In the above snippets note the type of match branch, that is the key difference in the types of iteration. The difference in type then of course implies differing actions that are able to be performed.

See also:

Iterator

match

Cairo provides pattern matching via the match keyword, which can be used like a C switch. The first matching arm is evaluated and all possible values must be covered.

A limitation of the match statement applied to integers is that the values in the arms must be sequential, starting from 0.

fn main() {
    let number = 13;
    // TODO ^ Try different values for `number`

    println!("Tell me about {}", number);
    match number {
        0 => println!("Zero!"),
        // Match a single value
        1 => println!("One!"),
        // Match several values
        2 | 3 | 4 | 5 => println!("In between 2 and 5"),
        _ => println!("Bigger than 5"),
        // TODO ^ Try commenting out this catch-all arm
    }

    let boolean = true;
    // Match is an expression too
    let binary = match boolean {
        // The arms of a match must cover all the possible values
        false => 0,
        true => 1,
        // TODO ^ Try commenting out one of these arms
    };

    println!("{} -> {}", boolean, binary);
}

Destructuring

A match block can destructure items in a variety of ways.

enums

An enum is destructured as:

#[derive(Drop)]
enum Color {
    // These 3 are specified solely by their name.
    Red,
    Blue,
    Green,
    // These likewise tie `u32` tuples to different names: color models.
    RGB: (u32, u32, u32),
    HSV: (u32, u32, u32),
    HSL: (u32, u32, u32),
    CMY: (u32, u32, u32),
    CMYK: (u32, u32, u32, u32),
}

fn main() {
    let color = Color::RGB((122, 17, 40));
    // TODO ^ Try different variants for `color`

    println!("What color is it?");
    // An `enum` can be destructured using a `match`.
    match color {
        Color::Red => println!("The color is Red!"),
        Color::Blue => println!("The color is Blue!"),
        Color::Green => println!("The color is Green!"),
        Color::RGB((r, g, b)) => println!("Red: {}, green: {}, and blue: {}!", r, g, b),
        Color::HSV((h, s, v)) => println!("Hue: {}, saturation: {}, value: {}!", h, s, v),
        Color::HSL((h, s, l)) => println!("Hue: {}, saturation: {}, lightness: {}!", h, s, l),
        Color::CMY((c, m, y)) => println!("Cyan: {}, magenta: {}, yellow: {}!", c, m, y),
        Color::CMYK((
            c, m, y, k,
        )) => println!("Cyan: {}, magenta: {}, yellow: {}, key (black): {}!", c, m, y, k),
        // Don't need another arm because all variants have been examined
    }
}

See also:

enum

structs

Similarly, a struct can be destructured as shown:

#[derive(Copy, Drop)]
struct Foo {
    x: (u32, u32),
    y: u32,
}

#[derive(Drop)]
struct Bar {
    foo: Foo,
}


fn main() {
    // Try changing the values in the struct to see what happens
    let _foo = Foo { x: (1, 2), y: 3 };

    let faa = Foo { x: (1, 2), y: 3 };

    // You do not need a match block to destructure structs:
    let Foo { x: x0, y: y0 } = faa;
    println!("Outside: x0 = {x0:?}, y0 = {y0}");

    // Destructuring works with nested structs as well:
    let bar = Bar { foo: faa };
    let Bar { foo: Foo { x: nested_x, y: nested_y } } = bar;
    println!("Nested: nested_x = {nested_x:?}, nested_y = {nested_y:?}");
}

See also:

Structs

if let

For some use cases, when matching enums, match is awkward. For example:

    // Make `optional` of type `Option<i32>`
    let optional = Some(7_i32);

    match optional {
        Some(i) => println!("This is a really long string and `{:?}`", i),
        _ => {},
        // ^ Required because `match` is exhaustive. Doesn't it seem
    // like wasted space?
    };

if let is cleaner for this use case and in addition allows various failure options to be specified:

fn main() {
    // All have type `Option<i32>`
    let number = Some(7_i32);
    let letter: Option<i32> = None;
    let emoticon: Option<i32> = None;

    // The `if let` construct reads: "if `let` destructures `number` into
    // `Some(i)`, evaluate the block (`{}`).
    if let Some(i) = number {
        println!("Matched {:?}!", i);
    }

    // If you need to specify a failure, use an else:
    if let Some(i) = letter {
        println!("Matched {:?}!", i);
    } else {
        // Destructure failed. Change to the failure case.
        println!("Didn't match a number. Let's go with a letter!");
    }

    // Provide an altered failing condition.
    let i_like_letters = false;

    if let Some(i) = emoticon {
        println!("Matched {:?}!", i);
        // Destructure failed. Evaluate an `else if` condition to see if the
    // alternate failure branch should be taken:
    } else if i_like_letters {
        println!("Didn't match a number. Let's go with a letter!");
    } else {
        // The condition evaluated false. This branch is the default:
        println!("I don't like letters. Let's go with an emoticon :)!");
    }
}

In the same way, if let can be used to match any enum value:

// Our example enum
#[derive(Drop)]
enum Foo {
    Bar,
    Baz,
    Qux: u32,
}

fn main() {
    // Create example variables
    let a = Foo::Bar;
    let b = Foo::Baz;
    let c = Foo::Qux(100);

    // Variable a matches Foo::Bar
    if let Foo::Bar = a {
        println!("a is foobar");
    }

    // Variable b does not match Foo::Bar
    // So this will print nothing
    if let Foo::Bar = b {
        println!("b is foobar");
    }

    // Variable c matches Foo::Qux which has a value
    // Similar to Some() in the previous example
    if let Foo::Qux(value) = c {
        println!("c is {}", value);
    }
}

Another benefit is that if let allows us to match non-parameterized enum variants. This is true even in cases where the enum doesn't implement or derive PartialEq. In such cases if Foo::Bar == a would fail to compile, because instances of the enum cannot be equated, however if let will continue to work.

Would you like a challenge? Fix the following example to use if let:

//TAG: does_not_compile

// This enum purposely neither implements nor derives PartialEq.
// That is why comparing Foo::Bar == a fails below.
enum Foo {
    Bar,
}

fn main() {
    let a = Foo::Bar;

    // Variable a matches Foo::Bar
    if Foo::Bar == a {
        // ^-- this causes a compile-time error. Use `if let` instead.
        println!("a is foobar");
    }
}

See also:

enum and Option

while let

Similar to if let, while let can make awkward match sequences more tolerable. Consider the following sequence that increments i:

fn main() {
    // Make `optional` of type `Option<i32>`
    let mut optional = Some(0_i32);

    // Repeatedly try this test.
    loop {
        match optional {
            // If `optional` destructures, evaluate the block.
            Some(i) => {
                if i > 9 {
                    println!("Greater than 9, quit!");
                    optional = None;
                } else {
                    println!("`i` is `{:?}`. Try again.", i);
                    optional = Some(i + 1);
                }
                // ^ Requires 3 indentations!
            },
            // Quit the loop when the destructure fails:
            _ => { break; },
            // ^ Why should this be required? There must be a better way!
        }
    }
}

Using while let makes this sequence much nicer:

fn main() {
    // Make `optional` of type `Option<i32>`
    let mut optional = Some(0_i32);

    // This reads: "while `let` destructures `optional` into
    // `Some(i)`, evaluate the block (`{}`). Else `break`.
    while let Some(i) = optional {
        if i > 9 {
            println!("Greater than 9, quit!");
            optional = None;
        } else {
            println!("`i` is `{:?}`. Try again.", i);
            optional = Some(i + 1);
        }
        // ^ Less rightward drift and doesn't require
    // explicitly handling the failing case.
    }
    // ^ `if let` had additional optional `else`/`else if`
// clauses. `while let` does not have these.
}

See also:

enum and Option

Functions

Functions are declared using the fn keyword. Its arguments are type annotated, just like variables, and, if the function returns a value, the return type must be specified after an arrow ->.

The final expression in the function will be used as return value. Alternatively, the return statement can be used to return a value earlier from within the function, even from inside loops or if statements.

Let's rewrite FizzBuzz using functions!

// Unlike other languages, there's no restriction on the order of function definitions
fn main() {
    // We can use this function here, and define it somewhere later
    fizzbuzz_to(100);
}

// Function that returns a boolean value
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
    // Corner case, early return
    if rhs == 0 {
        return false;
    }

    // This is an expression, the `return` keyword is not necessary here
    lhs % rhs == 0
}

// Functions that "don't" return a value, actually return the unit type `()`
fn fizzbuzz(n: u32) -> () {
    if is_divisible_by(n, 15) {
        println!("fizzbuzz");
    } else if is_divisible_by(n, 3) {
        println!("fizz");
    } else if is_divisible_by(n, 5) {
        println!("buzz");
    } else {
        println!("{}", n);
    }
}

// When a function returns `()`, the return type can be omitted from the
// signature
fn fizzbuzz_to(n: u32) {
    for n in 1..= n {
        fizzbuzz(n);
    }
}

Associated functions & Methods

Some functions are connected to a particular type. These come in two forms: associated functions, and methods. Associated functions are functions that are defined on a type generally, while methods are associated functions that are called on a particular instance of a type.

#[derive(Copy, Drop)]
struct Point {
    x: u64,
    y: u64,
}

// Implementation block, all `Point` associated functions & methods go in here
#[generate_trait]
impl PointImpl of PointTrait {
    // This is an "associated function" because this function is associated with
    // a particular type, that is, Point.
    //
    // Associated functions don't need to be called with an instance.
    // These functions are generally used like constructors.
    fn origin() -> Point {
        Point { x: 0, y: 0 }
    }

    // Another associated function, taking two arguments:
    fn new(x: u64, y: u64) -> Point {
        Point { x, y }
    }
}

#[derive(Copy, Drop)]
struct Rectangle {
    p1: Point,
    p2: Point,
}

#[generate_trait]
impl RectangleImpl of RectangleTrait {
    // This is a method
    // `self: @Rectangle` means we're taking a snapshot of the Rectangle instance. Because
    // `@Rectangle` is associated to the `self` keyword, it's the expected caller object.
    fn area(self: @Rectangle) -> u64 {
        // `self` gives access to the struct fields via the dot operator
        let Point { x: x1, y: y1 } = *self.p1;
        let Point { x: x2, y: y2 } = *self.p2;

        // Calculate absolute value using if/else since Cairo doesn't have abs()
        let width = if x1 >= x2 {
            x1 - x2
        } else {
            x2 - x1
        };
        let height = if y1 >= y2 {
            y1 - y2
        } else {
            y2 - y1
        };
        width * height
    }

    fn perimeter(self: @Rectangle) -> u64 {
        let Point { x: x1, y: y1 } = *self.p1;
        let Point { x: x2, y: y2 } = *self.p2;

        let width = if x1 >= x2 {
            x1 - x2
        } else {
            x2 - x1
        };
        let height = if y1 >= y2 {
            y1 - y2
        } else {
            y2 - y1
        };
        2 * (width + height)
    }

    // This method requires the caller object to be mutable
    // `ref self` means we're taking a reference to modify the Rectangle instance and return it to
    // the calling context.
    fn translate(ref self: Rectangle, x: u64, y: u64) {
        self.p1.x += x;
        self.p2.x += x;
        self.p1.y += y;
        self.p2.y += y;
    }
}

fn main() {
    let rectangle = Rectangle { // Associated functions are called using double colons
        p1: PointTrait::origin(), p2: PointTrait::new(3, 4),
    };

    // Methods are called using the dot operator
    // Note that the snapshot is implicitly passed
    println!("Rectangle perimeter: {}", rectangle.perimeter());
    println!("Rectangle area: {}", rectangle.area());

    let mut square = Rectangle { p1: PointTrait::origin(), p2: PointTrait::new(1, 1) };

    // Error! `rectangle` is immutable, but this method requires a mutable object
    // because `self` is taken by `ref`.
    // rectangle.translate(1, 0);
    // TODO ^ Try uncommenting this line

    // Okay! Mutable objects can call mutable methods
    square.translate(1, 1);
}

Methods must be defined within traits in Cairo, unlike Rust where they can be defined directly on types. We use the #[generate_trait] attribute to automatically generate the trait definition for us.

The main benefit of using methods instead of functions, in addition to providing method syntax, is for organization. We've put all the things we can do with an instance of a type in one impl block rather than making future users of our code search for capabilities of Point in various places in the library we provide.

Closures

Closures are functions that can capture the enclosing environment. For example, a closure that captures the x variable:

|val| val + x

The syntax and capabilities of closures make them very convenient for on-the-fly usage. Calling a closure is exactly like calling a function. However, both input and return types can be inferred and input variable names must be specified.

Other characteristics of closures include:

  • using || instead of () around input variables.
  • optional body delimitation ({}) for a single line expression (mandatory otherwise).
  • the ability to capture the outer environment variables.
fn main() {
    let outer_var = 42_u32;

    // A regular function can't be defined in the body of another function.
    // fn function(i: u32) -> u32 { i + outer_var };
    // TODO: uncomment the line above and see the compiler error.

    // Closures are anonymous, here we are binding them to references.
    // Annotation is identical to function annotation but is optional
    // as are the `{}` wrapping the body. These nameless functions
    // are assigned to appropriately named variables.
    let closure_inferred = |i| i + outer_var;

    // Call the closures
    println!("closure_inferred: {}", closure_inferred(1_u32));
    // Once closure's type has been inferred, it cannot be inferred again with another type.
    //println!("cannot reuse closure_inferred with another type: {}", closure_inferred(42_u64));
    // TODO: uncomment the line above and see the compiler error

    // A closure taking no arguments which returns a `u32`.
    // The return type is inferred.
    let one =  || 1_u32;
    println!("closure returning one: {}", one());
}

Capturing

Closures are inherently flexible and will do what the functionality requires to make the closure work without annotation. This allows capturing to flexibly adapt to the use case, sometimes moving and sometimes borrowing.

Closures can capture variables:

  • By snapshot: @T
  • By value: T

They preferentially capture variables by reference and only by value when required.

A restriction of closures is that they cannot capture mutable variables.

fn main() {
    let color: ByteArray = "green";

    // A closure to print `color` which immediately takes by snapshot (`@`) `color` and
    // stores the snapshot and closure in the `print` variable. It will remain
    // borrowed until `print` is used the last time.
    //
    // `println!` only requires arguments by snapshot so it doesn't
    // impose anything more restrictive.
    let print =  || println!("`color`: {}", color);

    // Call the closure using the borrow.
    print();

    // `into_iter` requires `T` so this must take by value. A copy type
    // would copy into the closure leaving the original untouched.
    // A non-copy must move the value into the closure.
    let consume = 
        || {
            println!("`color`: {}", color);
            let mut iter = color.into_iter();
            println!("`byte_0`: 0x{:x}", iter.next().unwrap());
        };

    // `consume` consumes the variable so this can only be called once.
    consume();
    // consume();
// ^ TODO: Try uncommenting this line.
}

As input parameters

While Cairo chooses how to capture variables on the fly mostly without type annotation, this ambiguity is not allowed when writing functions. When taking a closure as an input parameter, the closure's complete type must be annotated using one of two traits, and they're determined by what the closure does with captured value. In order of decreasing restriction, they are:

  • Fn: the closure uses the captured value by snapshot (@T)
  • FnOnce: the closure uses the captured value by value (T)

On a variable-by-variable basis, the compiler will capture variables in the least restrictive manner possible.

For instance, consider a parameter annotated as FnOnce. This specifies that the closure may capture by @T or T, but the compiler will ultimately choose based on how the captured variables are used in the closure.

This is because if a move is possible, then any type of snapshot should also be possible. Note that the reverse is not true. If the parameter is annotated as Fn, then capturing variables by T is not allowed. However, @T is allowed.

In the following example, try swapping the usage of Fn and FnOnce to see what happens:

Note: Cairo 2.10 provides an experimental feature allowing you to specify the associated type of trait, using experimental-features = ["associated_item_constraints"] in your Scarb.toml.

// A function which takes a closure as an argument and calls it.
// <F> denotes that F is a "Generic type parameter"
fn apply<F, +Drop<F>, impl func: core::ops::FnOnce<F, ()>, +Drop<func::Output>>(f: F) {
    // ^ TODO: Try changing this to `Fn`.
    f();
}

// A function which takes a closure and returns a `u32`.
fn apply_to_3<F, +Drop<F>, impl func: core::ops::Fn<F, (u32,)>[Output: u32]>(f: F) -> u32 {
    // The closure takes a `u32` and returns a `u32`.
    f(3)
}

fn main() {
    // A non-copy type.
    let greeting: ByteArray = "hello";
    let farewell: ByteArray = "goodbye";

    // // Capture 2 variables: `greeting` by snapshot and
    // // `farewell` by value.
    let diary = 
        || {
            // `greeting` is by snapshot: requires `Fn`.
            println!("I said {}.", greeting);

            // Using farewell by value requires `FnOnce`.
            // Convert farewell to uppercase to demonstrate value capture through `into_iter`
            let mut iter = farewell.into_iter();
            let uppercase: ByteArray = iter.map(|c| if c >= 'a' {
                c - 32
            } else {
                c
            }).collect();
            println!("Then I screamed {}!", uppercase.clone());
        };

    // Call the function which applies the closure.
    // apply(diary);
    diary();

    // `double` satisfies `apply_to_3`'s trait bound
    let double = |x: u32| 2 * x;

    println!("3 doubled: {}", apply_to_3(double));
}

See also:

Fn, Generics, and FnOnce

Type anonymity

Closures succinctly capture variables from enclosing scopes. Does this have any consequences? It surely does. Observe how using a closure as a function parameter requires generics, which is necessary because of how they are defined:

// `F` must be generic.
fn foo<F, +Drop<F>, impl func: core::ops::FnOnce<F, ()>, +Drop<func::Output>>(f: F) {
    f();
}

When a closure is defined, the compiler implicitly creates a new anonymous structure to store the captured variables inside, meanwhile implementing the functionality via one of the traits: Fn or FnOnce for this unknown type. This type is assigned to the variable which is stored until calling.

Since this new type is of unknown type, any usage in a function will require generics. However, an unbounded type parameter <T> would still be ambiguous and not be allowed. Thus, bounding by one of the traits: Fn or FnOnce (which it implements) is sufficient to specify its type. Additional bounds are required to ensure that the closure can be called:

  • F must implement Drop (or Destruct) to go out of scope
  • func::Output, the output type of the closure, must implement Drop, as it is not used in the following code
// `F` must implement `Fn` for a closure which takes no
// inputs and returns nothing - exactly what is required
// for `print`.
// `func::Output` must implement `Drop` to ensure the closure's output is properly disposed of
// `F` must implement `Drop` to ensure the closure itself is properly disposed of
fn apply<F, +Drop<F>, impl func: core::ops::Fn<F, ()>, +Drop<func::Output>>(f: F) {
    f();
}

fn main() {
    let x = 7_u8;

    // Capture `x` into an anonymous type and implement
    // `Fn` for it. Store it in `print`.
    let print =  || println!("{}", x);

    // apply(print);
    print();
}

See also:

[A thorough analysis][thorough_analysis], Fn, and FnOnce

Input functions

Since closures may be used as arguments, you might wonder if the same can be said about functions. Unfortunately, not (yet!).

// Define a function which takes a generic `F` argument
// bounded by `Fn`, and calls it
fn call_me<F, +Drop<F>, impl func: core::ops::Fn<F, ()>, +Drop<func::Output>>(f: F) {
    f();
}

// Define a wrapper function satisfying the `Fn` bound
fn function() {
    println!("I'm a function!");
}

fn main() {
    // Define a closure satisfying the `Fn` bound
    let closure =  || println!("I'm a closure!");

    closure();
    // call_me(closure);
// call_me(function);
// TODO: uncomment the line above and see the compiler error
}

See also:

Fn and FnOnce

Examples in std

This section contains a few examples of using closures from the core library.

Iterator::sum

Iterator::sum is a function which takes an iterator and returns the sum of all of its elements. Its signature:

    fn sum<+Destruct<T>, +Destruct<Self::Item>, +Sum<Self::Item>>(
        self: T,
    ) -> Self::Item;

Here's an example of how to use sum():

//TAG: does_not_compile
fn main() {
    let array = array![1_u8, 2, 3];
    let span = array![4_u8, 5, 6].span();

    // `into_iter()` for arrays yields elements by value
    println!("sum of array: {}", array.into_iter().sum());
    // `into_iter()` for spans yields elements by snapshot
    println!("sum of span: {}", span.into_iter().sum());

    // `into_iter` takes ownership of the value passed.
    // Because `Span` implements copy, it can be used again.
    println!("span length: {}", span.len());
    println!("First element of span: {}", span[0]);
    // Because `Array` doesn't implement copy, it can't be used again.
// println!("array length: {}", array.len()); // This would not compile
// println!("First element of array: {}", array[0]); // This would not compile
// TODO: uncomment the two lines above and see compiler errors.
}

See also:

core::iter::Iterator::sum

Searching through iterators

Iterator::find is a function which iterates over an iterator and searches for the first value which satisfies some condition. If none of the values satisfy the condition, it returns None. Its signature:

pub trait Iterator<T> {
    // The type being iterated over.
    type Item;

    // `find` takes `ref self` meaning the caller may be
    // modified, but not consumed.
    fn find<
        P,
        // `Fn` meaning that any captured variable will not be consumed. `@Self::Item` states it
        // takes arguments to the closure by snapshot.
        +core::ops::Fn<P, (@Self::Item,)>[Output: bool],
        +Destruct<P>,
        +Destruct<T>,
        +Destruct<Self::Item>,
    >(
        ref self: T, predicate: P,
    ) -> Option<
        Self::Item,
    >;
}
fn main() {
    let array = array![1_u8, 2, 3];
    let span = array![4_u8, 5, 6].span();

    // `into_iter()` for arrays yields elements by value
    let mut iter = array.into_iter();
    // `into_iter()` for spans yields elements by snapshot
    let mut into_iter = span.into_iter();

    // `into_iter()` for arrays yields `@T`, and we want to reference one of
    // its items, so we have to destructure `@T` to `T`
    println!("Find 2 in array: {:?}", iter.find(|x| *x == 2));
    // `into_iter()` for spans yields `@T`, and we want to reference one of
    // its items, so we have to compare with `@@T`
    println!("Find 2 in span: {:?}", into_iter.find(|x| x == @@2));
}

Iterator::find gives you a snapshot of the item.

See also:

std::iter::Iterator::find

Higher Order Functions

Cairo provides Higher Order Functions (HOF). These are functions that take one or more functions and/or produce a more useful function. HOFs and iterators give Cairo its functional flavor.

fn is_odd(n: u32) -> bool {
    n % 2 == 1
}

fn main() {
    println!("Find the sum of all the squared odd numbers under 1000");
    let upper = 1000;

    // Imperative approach
    // Declare accumulator variable
    let mut acc = 0_u32;
    // Iterate: 0, 1, 2, ... to infinity
    let mut n = 0_u32;
    loop {
        // Square the number
        let n_squared = n * n;

        if n_squared >= upper {
            // Break loop if exceeded the upper limit
            break;
        } else if is_odd(n_squared) {
            // Accumulate value, if it's odd
            acc += n_squared;
        }
        n += 1;
    };
    println!("imperative style: {}", acc);

    // Assumption: we can use the range (0..1000) because we know 1000^2 > 1000
    let sum_of_squared_odd_numbers = (0_usize..1000)
        .into_iter()
        .map(|n| n * n) // Square all numbers
        .filter(|n_squared| *n_squared < upper) // Take only those under 1000
        .filter(|n_squared| is_odd(*n_squared)) // Take only odd numbers
        .sum(); // Sum them all

    println!("functional style: {}", sum_of_squared_odd_numbers);
}

Option and Iterator implement their fair share of HOFs.

Modules

Cairo provides a powerful module system that can be used to hierarchically split code in logical units (modules), and manage visibility (public/private) between them.

A module is a collection of items: functions, structs, traits, impl blocks, and even other modules.

Visibility

By default, the items in a module have private visibility, but this can be overridden with the pub modifier. Only the public items of a module can be accessed from outside the module scope.

// A module named `my_mod`
mod my_mod {
    // Items in modules default to private visibility.
    fn private_function() {
        println!("called `my_mod::private_function()`");
    }

    // Use the `pub` modifier to override default visibility.
    pub fn function() {
        println!("called `my_mod::function()`");
    }

    // Items can access other items in the same module,
    // even when private.
    pub fn indirect_access() {
        print!("called `my_mod::indirect_access()`, that\n> ");
        private_function();
    }

    // Modules can also be nested
    pub mod nested {
        pub fn function() {
            println!("called `my_mod::nested::function()`");
        }

        fn private_function() {
            println!("called `my_mod::nested::private_function()`");
        }

        // Functions declared using `pub(crate)` syntax are only visible
        // within the given crate.
        pub(crate) fn public_function_in_crate() {
            println!("called `my_mod::nested::public_function_in_crate()`");
        }
    }

    // pub(crate) makes functions visible only within the current crate
    pub(crate) fn public_function_in_crate() {
        println!("called `my_mod::public_function_in_crate()`");
    }

    // Nested modules follow the same rules for visibility
    mod private_nested {
        pub fn function() {
            println!("called `my_mod::private_nested::function()`");
        }

        // Private parent items will still restrict the visibility of a child item,
        // even if it is declared as visible within a bigger scope.
        pub(crate) fn restricted_function() {
            println!("called `my_mod::private_nested::restricted_function()`");
        }
    }
}

fn function() {
    println!("called `function()`");
}

fn main() {
    // Modules allow disambiguation between items that have the same name.
    function();
    my_mod::function();

    // Public items, including those inside nested modules, can be
    // accessed from outside the parent module.
    my_mod::indirect_access();
    my_mod::nested::function();

    // pub(crate) items can be called from anywhere in the same crate
    my_mod::public_function_in_crate();
    // Private items of a module cannot be directly accessed, even if
// nested in a public module:

    // Error! `private_function` is private
// my_mod::private_function();
// TODO ^ Try uncommenting this line

    // Error! `private_function` is private
// my_mod::nested::private_function();
// TODO ^ Try uncommenting this line

    // Error! `private_nested` is a private module
// my_mod::private_nested::function();
// TODO ^ Try uncommenting this line

    // Error! `private_nested` is a private module
// my_mod::private_nested::restricted_function();
// TODO ^ Try uncommenting this line
}

Struct visibility

Structs have an extra level of visibility with their fields. The visibility defaults to private, and can be overridden with the pub modifier. This visibility only matters when a struct is accessed from outside the module where it is defined, and has the goal of hiding information (encapsulation).

mod my {
    // A public struct with a public field of generic type `T`
    #[derive(Drop)]
    pub struct OpenBox<T> {
        pub contents: T,
    }

    // A public struct with a private field of generic type `T`
    #[derive(Drop)]
    pub struct ClosedBox<T> {
        contents: T,
    }

    #[generate_trait]
    pub impl ClosedBoxImpl<T> of ClosedBoxTrait<T> {
        // Trait methods are public
        fn new(contents: T) -> ClosedBox<T> {
            ClosedBox { contents: contents }
        }
    }
}

fn main() {
    // Public structs with public fields can be constructed as usual
    let open_box = my::OpenBox::<ByteArray> { contents: "public information" };

    // and their fields can be normally accessed.
    println!("The open box contains: {}", open_box.contents);

    // Public structs with private fields cannot be constructed using field names.
    // Error! `ClosedBox` has private fields
    // let closed_box = my::ClosedBox::<ByteArray> { contents: "classified information" };
    // TODO ^ Try uncommenting this line

    // However, structs with private fields can be created using
    // public constructors
    let _closed_box: my::ClosedBox<ByteArray> = my::ClosedBoxTrait::new("classified information");
    // and the private fields of a public struct cannot be accessed.
// Error! The `contents` field is private
// println!("The closed box contains: {}", _closed_box.contents);
// TODO ^ Try uncommenting this line
}

See also:

generics and methods

The use declaration

The use declaration can be used to bind a full path to a new name, for easier access. It is often used like this:

use crate::deeply::nested::{my_first_function, my_second_function, AndAType};

fn main() {
    my_first_function();
}


You can use the as keyword to bind imports to a different name:

// Bind the `deeply::nested::function` path to `other_function`.
use deeply::nested::function as other_function;

fn function() {
    println!("called `function()`");
}

mod deeply {
    pub mod nested {
        pub fn function() {
            println!("called `deeply::nested::function()`");
        }
    }
}

fn main() {
    // Easier access to `deeply::nested::function`
    other_function();
    function();
}

super

The super keyword can be used in the path to remove ambiguity when accessing items and to prevent unnecessary hardcoding of paths.

fn function() {
    println!("called `function()`");
}

mod cool {
    pub fn function() {
        println!("called `cool::function()`");
    }
}

mod my {
    fn function() {
        println!("called `my::function()`");
    }

    mod cool {
        pub fn function() {
            println!("called `my::cool::function()`");
        }
    }

    pub fn indirect_call() {
        // Let's access all the functions named `function` from this scope!
        print!("called `my::indirect_call()`, that\n> ");

        function();

        // We can also access another module inside `my`:
        cool::function();

        // The `super` keyword refers to the parent scope (outside the `my` module).
        super::function();
    }
}

fn main() {
    my::indirect_call();
}

File hierarchy

Modules can be mapped to a file/directory hierarchy. Let's break down the visibility example in files:

$ tree .
.
├── src
│   ├── my
│   │   ├── inaccessible.cairo
│   │   └── nested.cairo
│   ├── my.cairo
│   └── lib.cairo
└── Scarb.toml

In src/lib.cairo:

// This declaration will look for a file named `my.cairo` and will
// insert its contents inside a module named `my` under this scope
mod my;

fn function() {
    println!("called `function()`");
}

fn main() {
    my::function();

    function();

    my::indirect_access();

    my::nested::function();
}

In src/my.cairo:

// Similarly `mod inaccessible` and `mod nested` will locate the `nested.cairo`
// and `inaccessible.cairo` files and insert them here under their respective
// modules
mod inaccessible;
pub mod nested;

pub fn function() {
    println!("called `my::function()`");
}

fn private_function() {
    println!("called `my::private_function()`");
}

pub fn indirect_access() {
    println!("called `my::indirect_access()`, that");
    println!("> ");
    private_function();
}

In src/my/inaccessible.cairo:

pub fn public_function() {
    println!("called `my::inaccessible::public_function()`");
}

Let's check that things still work as before:

$ scarb cairo-run 
warn: `scarb cairo-run` will be deprecated soon
help: use `scarb execute` instead
   Compiling split v0.1.0 (listings/mod/split/Scarb.toml)
    Finished `dev` profile target(s) in 3 seconds
     Running split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> 
called `my::private_function()`
called `my::nested::function()`
Run completed successfully, returning []

Crates

A crate is a single compilation unit. It has a root directory, and a root module defined at the file lib.cairo under this directory. In the case of a scarb package, the root directory is the src directory, and the root module is defined at the file lib.cairo.

Whenever scarb build is called, the src/lib.cairo is treated as the crate file. If src/lib.cairo has mod declarations in it, then the contents of the module files would be inserted in places where mod declarations in the crate file are found, before running the compiler over it. In other words, modules do not get compiled individually, only crates get compiled.

Because Cairo is strongly coupled to Scarb for its build system, we will use the terms "crate" and "package" interchangeably.

A crate can be compiled into a starknet contract or a library. By default, scarb build will produce a library from a package. This behavior can be overridden by specifying targets in the Scarb.toml file.

Scarb

Scarb is the official Cairo package management tool. It has lots of really useful features to improve code quality and developer velocity! These include

  • Dependency management and integration with scarbs.xyz (the official Scarb package registry)
  • Integration with Starknet Foundry for testing and benchmarks

This chapter will go through some quick basics, but you can find the comprehensive docs in The Scarb Documentation.

Dependencies

Most programs have dependencies on some libraries. If you have ever managed dependencies by hand, you know how much of a pain this can be. Luckily, the Cairo ecosystem comes standard with scarb! scarb can manage dependencies for a project.

To create a new Scarb project,

scarb new foo

The CLI will ask whether you want to use Starknet Foundry or Cairo Test as the test runner. Although Starknet Foundry is principally aimed at Starknet development, it presents useful features for pure Cairo development as well. Let's go with Starknet Foundry for this example.

After the above commands, you should see a file hierarchy like this:

foo
├── Scarb.lock
├── Scarb.toml
├── snfoundry.toml
├── src
│   └── lib.cairo
└── tests
    └── test_contract.cairo

The lib.cairo is the root source file for your new foo project -- nothing new there. The Scarb.toml is the config file for scarb for this project. If you look inside it, you should see something like this:

[package]
name = "dependencies"
version = "0.1.0"
edition = "2024_07"

The name field under [package] determines the name of the project. This is used by crates.io if you publish the crate (more later). It is also the name of the output binary when you compile.

The version field is a crate version number using Semantic Versioning.

The [dependencies] section lets you add dependencies for your project.

In our example, because we chose Starknet Foundry as the test runner, we have starknet as a dependency. But we could also add other dependencies.

For example, suppose that we want our program to handle fixed-point arithmetics. You can find lots of great packages on scarbs.xyz (the official Scarb package registry). A package choice for that use case would be is fp. As of this writing, the most recent published version of fp is 0.1.4. To add a dependency to our program, we can simply add the following to our Scarb.toml under [dependencies]: fp = "0.1.4". And that's it! You can start using fp in your program. We could also use the CLI to add the dependency:

scarb add [email protected]

scarb is more than a dependency manager. All of the available configuration options are listed in the format specification of Scarb.toml.

To build our project we can execute scarb build anywhere in the project directory (including subdirectories!). We can also do scarb cairo-run to build and run. Notice that these commands will resolve all dependencies, download crates if needed, and build everything, including your crate. (Note that it only rebuilds what it has not already built, similar to make).

Voila! That's all there is to it!

Testing

As we know testing is integral to any piece of software! Cairo has support for unit and integration testing (see this chapter in The Cairo Book).

Cairo has two different test runners, integrated with scarb:

  • Starknet Foundry, which is principally aimed at Starknet development, but presents useful features for pure Cairo development as well.
  • Cairo Test, which is a lightweight, pure Cairo test runner.

We'll use Starknet Foundry in the rest of these examples.

From the testing chapters linked above, we see how to write unit tests and integration tests. Organizationally, we can place unit tests in the modules they test and integration tests in their own tests/ directory:

foo
├── Scarb.toml
├── src
│   └── lib.cairo
└── tests
    ├── my_test.cairo
    └── my_other_test.cairo

Each file in tests is a separate integration test, i.e. a test that is meant to test your library as if it were being called from a dependent crate.

The Testing chapter elaborates on the two different testing styles: Unit, and Integration.

scarb naturally provides an easy way to run all of your tests!

$ scarb test

You should see output like this:

$ scarb test 
     Running test test (snforge test)
    Blocking waiting for file lock on registry db cache
    Blocking waiting for file lock on registry db cache
   Compiling test test v0.1.0 (listings/scarb/test/Scarb.toml)
   Compiling test(listings/scarb/test/Scarb.toml)
    Finished `dev` profile target(s) in 10 seconds


Collected 4 test(s) from test package
Running 4 test(s) from tests/
[PASS] test_tests::test_bar (gas: ~1)
[PASS] test_tests::test_foo_bar (gas: ~1)
[PASS] test_tests::test_foo (gas: ~1)
[PASS] test_tests::test_baz (gas: ~1)
Running 0 test(s) from src/
Tests: 4 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

You can also run tests whose name matches a pattern:

$ scarb test test_foo
$ scarb test test_foo
     Running test test (snforge test)
   Compiling test test v0.1.0 (listings/scarb/test/Scarb.toml)
   Compiling test(listings/scarb/test/Scarb.toml)
    Finished `dev` profile target(s) in 8 seconds


Collected 2 test(s) from test package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] test_tests::test_foo_bar (gas: ~1)
[PASS] test_tests::test_foo (gas: ~1)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 2 filtered out

Attributes

An attribute is metadata applied to some module, crate or item. This metadata can be used to/for:

Attributes look like #[outer_attribute]. They apply to the [item][item] immediately following it. Some examples of items are: a function, a module declaration, a constant, a structure, an enum. Here is an example where attribute #[derive(Debug)] applies to the struct Rectangle:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

Attributes can take arguments with different syntaxes:

  • #[attribute(key: "value")]
  • #[attribute(value)]

Attributes can have multiple values and can be separated over multiple lines, too:

#[attribute(value, value2)]


#[attribute(value, value2, value3,
            value4, value5)]

unused_imports

The compiler provides a unused_imports lint that will warn about unused imports. An attribute can be used to disable the lint.

// `#[allow(unused_imports)]` is an attribute that disables the `unused_imports` lint
use core::num::traits::ops::checked::CheckedAdd;
#[allow(unused_imports)]
use core::num::traits::ops::checked::CheckedSub;
use core::num::traits::ops::wrapping::WrappingMul;
// FIXME ^ Add an attribute to suppress the warning

fn main() {
    2_u8.checked_add(1).unwrap();
}

Note that in real programs, you should eliminate dead code. In these examples we'll allow dead code in some places because of the interactive nature of the examples.

cfg

Configuration conditional checks are possible through the cfg attribute #[cfg(...)]

This attribute enables conditional compilation, removing code that is not valid for the current configuration.

For example, a package supporting various hash functions might define features like this in the Scarb.toml file:

[package]
name = "cfg"
version = "0.1.0"
edition = "2024_07"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]

[dev-dependencies]
cairo_test = "2.9.2"

[features]
default = ["poseidon"]
poseidon = []
pedersen = []

And then use the features in the source code:

// This function only gets compiled if the feature "poseidon" is enabled
#[cfg(feature: "poseidon")]
fn which_hash_function() {
    println!("You are hashing with Poseidon!");
}

// This function only gets compiled if the feature "poseidon" is not enabled
#[cfg(not(feature: "poseidon"))]
fn which_hash_function() {
    println!("You are **not** hashing with Poseidon!");
}

fn main() {
    which_hash_function();
}

See also:

the Scarb reference

Generics

Generics is the topic of generalizing types and functionalities to broader cases. This is extremely useful for reducing code duplication in many ways, but can call for rather involved syntax. Namely, being generic requires taking great care to specify over which types a generic type is actually considered valid. The simplest and most common use of generics is for type parameters.

A type parameter is specified as generic by the use of angle brackets and upper camel case: <Aaa, Bbb, ...>. "Generic type parameters" are typically represented as <T>. In Cairo, "generic" also describes anything that accepts one or more generic type parameters <T>. Any type specified as a generic type parameter is generic, and everything else is concrete (non-generic).

For example, defining a generic function named foo that takes an argument T of any type:

fn foo<T>(arg: T) { ... }

Because T has been specified as a generic type parameter using <T>, it is considered generic when used here as (arg: T). This is the case even if T has previously been defined as a struct.

This example shows some of the syntax in action:

// A concrete type `A`.
#[derive(Copy, Drop)]
struct A {}

// In defining the type `Single`, the first use of `A` is not preceded by `<A>`.
// Therefore, `Single` is a concrete type, and `A` is defined as above.
#[derive(Copy, Drop)]
struct Single {
    value: A,
}
//            ^ Here is `Single`s first use of the type `A`.

// Here, `<T>` precedes the first use of `T`, so `SingleGen` is a generic type.
// Because the type parameter `T` is generic, it could be anything, including
// the concrete type `A` defined at the top.
#[derive(Copy, Drop)]
struct SingleGen<T> {
    value: T,
}

fn main() {
    // `Single` is concrete and explicitly takes `A`.
    let _s = Single { value: A {} };

    // Create a variable `_char` of type `SingleGen<felt252>`
    // and give it the value `SingleGen('a')`.
    // Here, `SingleGen` has a type parameter explicitly specified.
    let _felt: SingleGen<felt252> = SingleGen { value: 'a' };

    // `SingleGen` can also have a type parameter implicitly specified:
    let _t = SingleGen { value: A {} }; // Uses `A` defined at the top.
    let _i32 = SingleGen { value: 6 }; // Uses `felt252`.
    let _felt = SingleGen { value: 'a' }; // Uses `felt252`.
}

See also:

structs

Functions

The same set of rules can be applied to functions: a type T becomes generic when preceded by <T>.

Using generic functions sometimes requires explicitly specifying type parameters. This may be the case if the function is called where the return type is generic, or if the compiler doesn't have enough information to infer the necessary type parameters.

A function call with explicitly specified type parameters looks like: fun::<A, B, ...>().

#[derive(Drop)]
struct A {} // Concrete type `A`.
#[derive(Drop)]
struct S { // Concrete type `S`.
    value: A,
}
#[derive(Drop)]
struct SGen<T> { // Generic type `SGen`.
    value: T,
}

// The following functions all take ownership of the variable passed into
// them and immediately go out of scope, freeing the variable.

// Define a function `reg_fn` that takes an argument `_s` of type `S`.
// This has no `<T>` so this is not a generic function.
fn reg_fn(_s: S) {}

// Define a function `gen_spec_t` that takes an argument `_s` of type `SGen<T>`.
// It has been explicitly given the type parameter `A`, but because `A` has not
// been specified as a generic type parameter for `gen_spec_t`, it is not generic.
fn gen_spec_t(_s: SGen<A>) {}

// Define a function `gen_spec_i32` that takes an argument `_s` of type `SGen<i32>`.
// It has been explicitly given the type parameter `i32`, which is a specific type.
// Because `i32` is not a generic type, this function is also not generic.
fn gen_spec_i32(_s: SGen<i32>) {}

// Define a function `generic` that takes an argument `_s` of type `SGen<T>`.
// Because `SGen<T>` is preceded by `<T>`, this function is generic over `T`.
// We need T to implement the Drop trait to go out of scope.
fn generic<T, impl TDrop: Drop<T>>(_s: SGen<T>) {}

fn main() {
    // Using the non-generic functions
    reg_fn(S { value: A {} }); // Concrete type.
    gen_spec_t(SGen { value: A {} }); // Implicitly specified type parameter `A`.
    gen_spec_i32(SGen { value: 6 }); // Implicitly specified type parameter `i32`.

    // Explicitly specified type parameter `felt252` to `generic()`.
    generic::<felt252>(SGen { value: 'a' });

    // Implicitly specified type parameter `felt252` to `generic()`.
    generic(SGen { value: 'c' });
}

See also:

functions and structs

Traits

Of course traits can also be generic. Here we define one which implements a generic method to drop itself and an input.

// Non-copyable types
#[derive(Drop)]
struct Empty {}

#[derive(Drop)]
struct Null {}

// A trait generic over `T, U`.
trait DoubleDrop<T, U> {
    // Define a method on the caller type which takes an
    // additional single parameter `U` and does nothing with it.
    fn double_drop(self: T, _x: U);
}

// Implement `DoubleDrop<T>` for any generic parameter `T` and
// caller `U`.
impl DoubleDropImpl<T, U, +Drop<T>, +Drop<U>> of DoubleDrop<T, U> {
    // This method takes ownership of both passed arguments,
    // deallocating both.
    fn double_drop(self: T, _x: U) {}
}

fn main() {
    let empty = Empty {};
    let null = Empty {};

    // Move `empty` and `null` and drop them.
    empty.double_drop(null);
    // empty.double_drop(null);
// null.double_drop(empty);
// ^ TODO: Try uncommenting these lines.
}

See also:

Drop, struct, and trait

Implementation

Similar to functions, implementations require care to remain generic.

struct S {} // Concrete type `S`

trait T {} // Concrete trait `T`
trait GenericValTrait<T> {} // Generic trait `GenericValTrait`

// impl of GenericVal where we explicitly specify type parameters:
impl GenericValU32 of GenericValTrait<u32> {} // Specify `u32`
impl GenericValS of GenericValTrait<S> {} // Specify `S` as defined above

// `<T>` Must precede the type to remain generic
impl GenericValImpl<T> of GenericValTrait<T> {}
#[derive(Copy, Drop)]
struct Val {
    val: u64,
}

#[derive(Copy, Drop)]
struct GenVal<T> {
    gen_val: T,
}

// impl of Val
trait ValTrait {
    fn value(self: @Val) -> @u64;
}

impl ValImpl of ValTrait {
    fn value(self: @Val) -> @u64 {
        self.val
    }
}

// impl of GenVal for a generic type `T`
trait GenValTrait<T> {
    fn value(self: @GenVal<T>) -> @T;
}

impl GenValImpl<T> of GenValTrait<T> {
    fn value(self: @GenVal<T>) -> @T {
        self.gen_val
    }
}

fn main() {
    let x = Val { val: 3 };
    let y = GenVal { gen_val: 3_u32 };

    println!("{}, {}", x.value(), y.value());
}

See also:

impl, and struct

Bounds

When working with generics, the type parameters often must use traits as bounds to stipulate what functionality a type implements. For example, the following example uses the trait Display to print and so it requires T to be bound by Display; that is, T must implement Display.

// Define a function `printer` that takes a generic type `T` which
// must implement trait `Display`.
fn printer<T, +core::fmt::Display<T>, +Drop<T>>(t: T) {
    println!("{}", t);
}

Bounding restricts the generic to types that conform to the bounds. That is:

//TAG: does_not_compile
#[derive(Drop)]
struct S<T, +core::fmt::Display<T>> {
    value: T,
}

// Error! `Array<T>` does not implement `Display`. This
// specialization will fail.
fn foo() {
    let _s = S { value: array![1] };
}

Another effect of bounding is that generic instances are allowed to access the methods of traits specified in the bounds. For example:

// A trait which implements the print marker: `{:?}`.
use core::fmt::Debug;

trait HasArea<T> {
    fn area(self: @T) -> u64;
}

#[derive(Debug, Drop)]
struct Rectangle {
    length: u64,
    height: u64,
}

#[derive(Drop)]
struct Triangle {
    length: u64,
    height: u64,
}

impl RectangleArea of HasArea<Rectangle> {
    fn area(self: @Rectangle) -> u64 {
        *self.length * *self.height
    }
}

// The generic `T` must implement `Debug`. Regardless
// of the type, this will work properly.
fn print_debug<T, +Debug<T>>(t: @T) {
    println!("{:?}", t);
}

// `T` must implement `HasArea`. Any type which meets
// the bound can access `HasArea`'s function `area`.
fn area<T, +HasArea<T>>(t: @T) -> u64 {
    HasArea::area(t)
}

fn main() {
    let rectangle = Rectangle { length: 3, height: 4 };
    let _triangle = Triangle { length: 3, height: 4 };

    print_debug(@rectangle);
    println!("Area: {}", area(@rectangle));
    //print_debug(@_triangle);
//println!("Area: {}", area(@_triangle));
// ^ TODO: Try uncommenting these.
// | Error: Does not implement either `Debug` or `HasArea`.
}

As an additional note, trait bounds in Cairo are specified using the + syntax after the generic type parameter. Multiple bounds can be specified by adding additional + constraints.

See also:

core::fmt, structs, and traits

Testcase: empty bounds

A consequence of how bounds work is that even if a trait doesn't include any functionality, you can still use it as a bound. Drop and Copy are examples of such traits from the core library.

#[derive(Drop)]
struct Cardinal {}

#[derive(Drop)]
struct BlueJay {}

#[derive(Drop)]
struct Turkey {}

trait Red<T> {}
trait Blue<T> {}

impl CardinalRed of Red<Cardinal> {}
impl BlueJayBlue of Blue<BlueJay> {}

// These functions are only valid for types which implement these
// traits. The fact that the traits are empty is irrelevant.
fn red<T, +Red<T>>(t: @T) -> ByteArray {
    "red"
}
fn blue<T, +Blue<T>>(t: @T) -> ByteArray {
    "blue"
}

fn main() {
    let cardinal = Cardinal {};
    let blue_jay = BlueJay {};
    let _turkey = Turkey {};

    // `red()` won't work on a blue jay nor vice versa
    // because of the bounds.
    println!("A cardinal is {}", red(@cardinal));
    println!("A blue jay is {}", blue(@blue_jay));
    //println!("A turkey is {}", red(@_turkey));
// ^ TODO: Try uncommenting this line.
}

See also:

core::traits::Drop, core::traits::Copy, and traits

Multiple bounds

Multiple bounds for a single type can be applied with additional + symbols. Like normal, different bounds are separated with ,.

use core::fmt::{Debug, Display};

// T must implement both Debug and Display
fn compare_prints<T, +Debug<T>, +Display<T>>(t: @T) {
    println!("Debug: `{:?}`", t);
    println!("Display: `{}`", t);
}

// T and U must both implement Debug
fn compare_types<T, U, +Debug<T>, +Debug<U>>(t: @T, u: @U) {
    println!("t: `{:?}`", t);
    println!("u: `{:?}`", u);
}

fn main() {
    let string: ByteArray = "words";
    let array = array![1, 2, 3];
    let vec = array![1, 2, 3];

    compare_prints(@string);
    // compare_prints(@array);
    // TODO ^ Try uncommenting this line.
    // Error: Array does not implement Display

    compare_types(@array, @vec);
}

See also:

core::fmt and traits

Associated items

"Associated Items" refers to a set of rules pertaining to items of various types. It is an extension to trait generics, and allows traits to internally define new items.

One such item is called an associated type, providing simpler usage patterns when the trait is generic over its container type.

See also:

Cairo Book

The Problem

A trait that is generic over its container type has type specification requirements - users of the trait must specify all of its generic types.

In the example below, the Contains trait allows the use of the generic types A and B, and can be implemented for any container C. The trait is then implemented for the concrete Container type, specifying i32 for A and B so that it can be used with fn difference().

Because Contains is generic, we are forced to explicitly state all of the generic types for fn difference(). In practice, we want a way to express that A and B are determined by the input C. As you will see in the next section, associated types provide exactly that capability.

#[derive(Drop)]
struct Container {
    first: i32,
    last: i32,
}

// A trait which checks if 2 items `A` and `B` are stored inside of container `C`.
// Also retrieves first or last value.
trait Contains<C, A, B> {
    fn contains(self: @C, a: @A, b: @B) -> bool; // Explicitly requires `A` and `B`.
    fn first(self: @C) -> i32; // Doesn't explicitly require `A` or `B`.
    fn last(self: @C) -> i32; // Doesn't explicitly require `A` or `B`.
}

impl ContainsImpl of Contains<Container, i32, i32> {
    // True if the numbers stored are equal.
    fn contains(self: @Container, a: @i32, b: @i32) -> bool {
        (self.first == a) && (self.last == b)
    }

    // Grab the first number.
    fn first(self: @Container) -> i32 {
        *self.first
    }

    // Grab the last number.
    fn last(self: @Container) -> i32 {
        *self.last
    }
}

// `C` contains `A` and `B`. In light of that, having to express `A` and
// `B` again is a nuisance.
fn difference<A, B, C, +Contains<C, A, B>>(container: @C) -> i32 {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container { first: number_1, last: number_2 };

    println!(
        "Does container contain {} and {}: {}",
        number_1,
        number_2,
        container.contains(@number_1, @number_2),
    );
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(@container));
}

See also:

structs, and traits

Associated types

The use of "Associated types" improves the overall readability of code by moving inner types locally into a trait as output types. Syntax for the trait definition is as follows:

// `A` and `B` are defined in the trait via the `type` keyword.
// (Note: `type` in this context is different from `type` when used for
// aliases).
trait Contains<T> {
    type A;
    type B;

    // Updated syntax to refer to these new types generically.
    fn contains(self: @T, a: @Self::A, b: @Self::B) -> bool;
}

Note that functions that use the trait Contains are no longer required to express A or B at all:

// Without using associated types
fn difference<A, B, C, +Contains<C, A, B>>(container: @C) -> i32 { ... }

// Using associated types
fn difference<T, +Contains<T>>(container: @T) -> i32 { ... }

Let's rewrite the example using associated types:

#[derive(Drop)]
struct Container {
    first: i32,
    last: i32,
}

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains<T> {
    // Define generic types here which methods will be able to utilize.
    type A;
    type B;

    fn contains(self: @T, a: @Self::A, b: @Self::B) -> bool;
    fn first(self: @T) -> i32;
    fn last(self: @T) -> i32;
}

impl ContainerImpl of Contains<Container> {
    // Specify what types `A` and `B` are. If the `input` type
    // is `Container{first: i32, last: i32}`, the `output` types are determined
    // as `i32` and `i32`.
    type A = i32;
    type B = i32;

    // `@Self::A` and `@Self::B` are also valid here.
    fn contains(self: @Container, a: @i32, b: @i32) -> bool {
        self.first == a && self.last == b
    }

    // Grab the first number.
    fn first(self: @Container) -> i32 {
        *self.first
    }

    // Grab the last number.
    fn last(self: @Container) -> i32 {
        *self.last
    }
}

fn difference<T, +Contains<T>>(container: @T) -> i32 {
    container.last() - container.first()
}

fn main() {
    let number_1: i32 = 3;
    let number_2: i32 = 10;

    let container = Container { first: number_1, last: number_2 };

    println!(
        "Does container contain {} and {}: {}",
        number_1,
        number_2,
        container.contains(@number_1, @number_2),
    );
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(@container));
}

Scoping rules

Scopes play an important part in ownership and snapshots. That is, they indicate to the compiler when snapshots are valid, when values can be dropped, and when variables are created or destroyed.

RAII

Variables in Cairo do more than just hold data in memory: they also own resources, e.g. Box<T> owns a pointer to memory, Felt252Dict owns a memory segment. Cairo enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destructor is called to handle the cleanup of its owned resources.

This behavior protects against soundness issues and concurrent memory accesses, ensuring that:

  1. It would be infeasible for a cheating prover to convince a verifier of a false statement, as some resources (like dictionaries) require specialized destruction logic.
  2. No memory cell is written to twice due to concurrent accesses, which would crash the VM.

Here's a quick showcase:

fn create_box() {
    // Write an integer to memory and return a pointer to it
    let _box1 = BoxTrait::new(3_u32);
    // `_box1` is dropped here: the associated pointer will no longer be accessible
}

fn main() {
    // Allocate a memory segment for an array and return a pointer to it
    let _box2 = BoxTrait::new(array![1_u8]);

    // A nested scope:
    {
        // Write an integer to memory and return a pointer to it
        let _box3 = BoxTrait::new(4_u32);

        let _pointee_2 = _box2.unbox();
        println!("Pointee 2 is: {:?}", _pointee_2);
        // `_box3` is dropped here: the associated pointer will no longer be accessible
    }
    // `_box2` has been moved to the inner scope when we accessed its inner resources, so it can no
// longer be accessed.
// let pointee_2 = _box2.unbox();
// TODO: uncomment this line and notice the compiler error.
}

Destructor

The notion of a destructor in Rust is provided through the Drop and Destruct traits. The destructor is called when the resource goes out of scope. One of the two traits is required to be implemented for every type:

  • If your type does not require any specific destructor logic, implement the Drop trait, which can trivially be derived.
  • If your type requires does specific destructor logic, applicable to any type containing a dictionary, implement the Destruct trait. It can also be derived.

Run the below example to see how the Drop trait works. When the variables in the main function goes out of scope the custom destructor will be invoked. Try to fix the error for the variable of type ToDestruct.

//TAG: does_not_compile

use core::dict::Felt252Dict;

#[derive(Drop)]
struct ToDrop {}

struct ToDestruct {
    inner: Felt252Dict<u8>,
}

fn main() {
    let _to_drop = ToDrop {};
    let _to_destruct = ToDestruct { inner: Default::default() };
    // TODO: modify the definition of ToDestruct to fix the error.
}

See also:

Box

Ownership and moves

Because Cairo uses a linear type system, values must be used exactly once. This means that when passing function arguments (foo(x)), the value is moved from one variable to another. After a value is moved, the previous variable can no longer be used.

This is similar to Rust's ownership system, but for different reasons. While Rust's ownership prevents data races and memory safety issues, Cairo's linear type system ensures code provability and abstracts the VM's immutable memory model.

// This function takes ownership of the array, because `Array<u128>` does not implement `Copy`.
fn pass_by_value_move(arr: Array<u128>) {
    println!("dropped an array of length {}", arr.len());
    // `arr` is dropped when it goes out of scope
}

// This function does not take ownership of the value,
// because `u128` implements `Copy`.
fn pass_by_value_copy(c: u128) {
    println!("dropped a value that contains {}", c);
    // `c` is dropped when it goes out of scope
}

fn main() {
    // Simple integer value
    let x = 5_u128;

    // Pass `x` by value to a function: because `u128` implements `Copy`, ownership is not moved.
    pass_by_value_copy(x);
    println!("x is {}", x);

    // Create an array (arrays don't implement Copy)
    let mut arr = array![5];

    println!("arr contains: {:?}", arr);

    // *Move* `arr` into `pass_by_value_move`
    pass_by_value_move(arr);
    // Error! `arr` can no longer be used since it was moved
// println!("arr contains: {:?}", arr));
// TODO ^ Try uncommenting this line

}

Mutability

Mutability of data can be changed when ownership is transferred.

fn modify_array_mut(mut mutable_array: Array<u8>) {
    mutable_array.append(4);
    println!("mutable_array now contains {:?}", mutable_array);
}

fn main() {
    let immutable_array = array![1, 2, 3];

    println!("immutable_array contains {:?}", immutable_array);

    // Mutability error
    // arr.append(4);

    // *Move* the array, changing the ownership (and mutability)
    modify_array_mut(immutable_array);
}

Retaining Ownership

Most of the time, we'd like to access data without taking ownership over it. To accomplish this, Cairo proposes two mechanism:

  • Snapshots: Instead of passing objects by value (T), objects can be passed by snapshot (@T). A snapshot is an immutable view into memory cells at a specific state that can not be mutated.
  • References: Instead of passing objects by value (T), objects can be passed by reference (ref T). A reference is simply a syntactic sugar for a variable whose ownership is transferred, can be mutated, and returned back to the original owner.

Snapshots

Most of the time, we'd like to access data without taking ownership over it. To accomplish this, Cairo uses a snapshot mechanism. Instead of passing objects by value (T), objects can be passed by snapshot (@T).

The compiler statically guarantees that snapshots always point to valid values. Since Cairo's memory is immutable, snapshots are simply views into memory cells at a specific state.

#[derive(Drop)]
struct NonCopyU8 {
    inner: u8,
}

// This function takes ownership of type that is not copyable
fn eat_noncopy_u8(noncopy_u8: NonCopyU8) {
    println!("Dropping a non copyable type that contains {}", noncopy_u8.inner);
}

// This function takes a snapshot of a non-copyable type
fn snapshot_noncopy(snapshot_noncopy_u8: @NonCopyU8) {
    println!("This non-copyable type contains {}", snapshot_noncopy_u8.inner);
}

// This function takes a snapshot of a copyable type
fn snapshot_copyable(snapshot_copyable_u8: @u8) {
    println!("This copyable type contains {}", snapshot_copyable_u8);
}

fn main() {
    // Create a both a copyable and non-copyable type.
    let copyable_u8 = 5_u8;
    let noncopy_u8 = NonCopyU8 { inner: 5_u8 };

    // Take snapshots of the contents. Ownership is not taken,
    // so the contents can be snapshotted again.
    snapshot_noncopy(@noncopy_u8);
    snapshot_copyable(@copyable_u8);

    {
        // Take a snapshot of the data contained inside the box
        let snap_to_noncopy: @NonCopyU8 = @noncopy_u8;

        // This is allowed! Snapshots are immutable views
        // so we can still use noncopy_u8 even while snapshots exist
        eat_noncopy_u8(noncopy_u8);

        // We can still use the snapshot after the original variable is dropped
        // since snapshots are just views of immutable memory cells
        snapshot_noncopy(snap_to_noncopy);
        // `snap_to_noncopy` goes out of scope
    }
    // We can't use noncopy_u8 here since ownership was transferred to eat_noncopy_u8
// eat_noncopy_u8(noncopy_u8); // This would fail to compile
}

References

Memory is immutable by default due to the underlying VM architecture. However, we can abstract this immutability using ref parameters, which allow us to modify a value and implicitly return ownership back to the caller.

#[derive(Drop)]
struct Book {
    author: ByteArray,
    title: ByteArray,
    year: u32,
}

// This function takes a snapshot of a book
fn borrow_book(book: @Book) {
    println!("I took a snapshot of {} - {} edition", book.title, book.year);
}

// This function takes a reference to a book and changes `year` to 2014
fn new_edition(ref book: Book) {
    book.year = 2014;
    println!("I modified {} - {} edition", book.title, book.year);
}

fn main() {
    // Create a Book
    let mut book = Book { author: "Douglas Hofstadter", title: "Godel, Escher, Bach", year: 1979 };

    // Take a snapshot of the book
    borrow_book(@book);

    // Pass a reference to modify the book
    new_edition(ref book);

    // We can still use book here since ownership was returned
    println!("Book is now from {}", book.year);
}

See also:

Snapshots and References

Traits

A trait is a collection of functions. Unlike in Rust, traits are not defined for a specific implementor type; they can contain methods for different types.

In practice, we often define a trait for a generic type T, and then implement that trait for specific types.

In the example below, we define Animal, a group of methods over a generic type T. The Animal trait is then implemented for the Sheep data type, allowing the use of methods from Animal with a Sheep.

The Sheep data type also has some methods. In Cairo, type methods are defined in traits, where the self type is the type itself. As such, we use the #[generate_trait] attribute to automatically generate a trait from the definition of an impl containing type methods.

What enables the method syntax is the self keyword, which can be used to refer to the implementor type. In that case, any type T that implements Animal can use the methods defined in Animal.

#[derive(Drop)]
struct Sheep {
    naked: bool,
    name: ByteArray,
}

trait Animal<T> {
    // Associated function signature; `T` refers to the implementor type
    fn new(name: ByteArray) -> T;

    // Method signatures; these will return a ByteArray
    fn name(self: @T) -> ByteArray;
    fn noise(self: @T) -> ByteArray;

    // Traits can provide default method definitions
    fn talk(self: @T) {
        println!("{} says {}", Self::name(self), Self::noise(self));
    }
}


// The `#[generate_trait]` attribute is used to automatically generate a trait from the definition
// of an impl.
// This pattern is often used to define methods on a type.
#[generate_trait]
impl SheepImpl of SheepTrait {
    fn is_naked(self: @Sheep) -> bool {
        *self.naked
    }

    fn shear(ref self: Sheep) {
        if self.is_naked() {
            // Implementor methods can use the implementor's trait methods
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);
            self.naked = true;
        }
    }
}

// Implement the `Animal` trait for `Sheep`
impl SheepAnimal of Animal<Sheep> {
    // `T` is the implementor type: `Sheep`
    fn new(name: ByteArray) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(self: @Sheep) -> ByteArray {
        self.name.clone()
    }

    fn noise(self: @Sheep) -> ByteArray {
        if self.is_naked() {
            "baaaaah?" // Questioning
        } else {
            "baaaaah!" // Confident
        }
    }

    // Default trait methods can be overridden
    fn talk(self: @Sheep) {
        // For example, we can add some quiet contemplation
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

fn main() {
    // Type annotation is necessary in this case
    let mut dolly: Sheep = Animal::new("Dolly");
    // TODO ^ Try removing the type annotations

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Derive

The compiler is capable of providing basic implementations for some traits via the #[derive] attribute. These traits can still be manually implemented if a more complex behavior is required.

The following is a list of derivable traits:

  • Comparison traits: PartialEq
  • Clone, to create a copy of a value
  • Copy, to give a type 'copy semantics' instead of 'move semantics'
  • Drop, to enable moving out of scope
  • Default, to create an empty instance of a data type
  • Debug, to format a value using the {:?} formatter
  • Display, to format a value using the {} formatter
  • Hash, to compute a hash from a value
  • Serde, to serialize and deserialize data structures
  • Store, to enable Starknet storage capabilities
// `Centimeters`, a tuple struct that can be equality-compared
#[derive(Copy, Drop, PartialEq)]
struct Centimeters {
    inner: u64,
}

// `Inches`, a tuple struct that can be printed
#[derive(Copy, Drop, Debug)]
struct Inches {
    inner: u64,
}

#[generate_trait]
impl InchesImpl of InchesTrait {
    fn to_centimeters(self: @Inches) -> Centimeters {
        let Inches { inner: inches } = *self;
        Centimeters {
            inner: inches * 254 / 100,
        } // Convert to centimeters (2.54 * 100 to avoid floats)
    }
}

// `Seconds`, a tuple struct with no additional attributes
#[derive(Drop)]
struct Seconds {
    inner: u64,
}

fn main() {
    let _one_second = Seconds { inner: 1 };

    // Error: `Seconds` can't be printed; it doesn't implement the `Debug` trait
    //println!("One second looks like: {:?}", _one_second);
    // TODO ^ Try uncommenting this line

    // Error: `Seconds` can't be compared; it doesn't implement the `PartialEq` trait
    //let _this_is_true = (_one_second == _one_second);
    // TODO ^ Try uncommenting this line

    let foot = Inches { inner: 12 };

    println!("One foot equals {:?}", foot);

    let meter = Centimeters { inner: 100 };

    let cmp: ByteArray = if foot.to_centimeters() == meter {
        "equal"
    } else {
        "not equal"
    };

    println!("One foot is {} to one meter.", cmp);
}

See also:

derive

Operator Overloading

In Cairo, many operators can be overloaded via traits. That is, some operators can be used to accomplish different tasks based on their input arguments. This is possible because operators are syntactic sugar for method calls. For example, the + operator in a + b calls the add method (as in a.add(b)). This add method is part of the Add trait. Hence, the + operator can be used by any implementor of the Add trait.

A list of the traits that overload operators can be found in core::ops and core::traits.

struct Potion {
    health: u64,
    mana: u64,
}

// The `Add` trait is used to specify the functionality of `+`.
// Here, we make `Add<Potion>` - the trait for addition with a LHS and RHS of type `Potion`.
// The following block implements the operation: Potion + Potion = Potion
impl PotionAdd of Add<Potion> {
    fn add(lhs: Potion, rhs: Potion) -> Potion {
        Potion { health: lhs.health + rhs.health, mana: lhs.mana + rhs.mana }
    }
}

fn main() {
    let health_potion: Potion = Potion { health: 100, mana: 0 };
    let mana_potion: Potion = Potion { health: 0, mana: 100 };
    let super_potion: Potion = health_potion + mana_potion;
    // Both potions were combined with the `+` operator.
    assert(super_potion.health == 100, '');
    assert(super_potion.mana == 100, '');
}

See Also

Add, Operators

Drop and Destruct

The Drop trait only has one method: drop, which is called automatically when an object goes out of scope. The main use of the Drop trait is to ensure that all dictionaries are "squashed" when they go out of scope. This "squashing" mechanism ensures that the sequential accesses to the dictionary are consistent and sound regarding proof generation.

Any type that is not a dictionary can trivially derive the Drop trait.

The [Destruct][Destruct] trait is a more powerful version of the Drop trait. It allows for the developer to specify what should happen when an object goes out of scope.

All types can trivially derive the Destruct trait, even if they're composed of dictionaries.

The following example adds a print to console to the drop function to announce when it is called.

use core::dict::Felt252Dict;

#[derive(Destruct)]
struct DestructibleType {
    name: ByteArray,
    dict: Felt252Dict<u64>,
}
// Try to derive `Drop` instead of `Destruct` and see what happens.

fn main() {
    let _a = DestructibleType { name: "a", dict: Default::default() };

    // block A
    {
        let _b = DestructibleType { name: "b", dict: Default::default() };

        // block B
        {
            let _c = DestructibleType { name: "c", dict: Default::default() };
            let _d = DestructibleType { name: "d", dict: Default::default() };

            println!("Exiting block B");
        }
        println!("Just exited block B");

        println!("Exiting block A");
    }
    println!("Just exited block A");

    println!("end of the main function");
}

Iterators

The Iterator trait is used to implement iterators over collections such as arrays.

The trait requires only a method to be defined for the next element, which may be manually defined in an impl block or automatically defined (as in arrays and ranges).

As a point of convenience for common situations, the for construct turns some collections into iterators using the .into_iter() method.

#[derive(Drop)]
struct Fibonacci {
    curr: u32,
    next: u32,
}

// Implement `Iterator` for `Fibonacci`.
// The `Iterator` trait only requires a method to be defined for the `next` element,
// and an `associated type` to declare the return type of the iterator.
impl FibonacciImpl of Iterator<Fibonacci> {
    // We can refer to this type using Self::Item
    type Item = u32;

    // Here, we define the sequence using `.curr` and `.next`.
    // The return type is `Option<T>`:
    //     * When the `Iterator` is finished, `None` is returned.
    //     * Otherwise, the next value is wrapped in `Some` and returned.
    // We use Self::Item in the return type, so we can change
    // the type without having to update the function signatures.
    fn next(ref self: Fibonacci) -> Option<Self::Item> {
        let current = self.curr;

        self.curr = self.next;
        self.next = current + self.next;

        // Since there's no endpoint to a Fibonacci sequence, the `Iterator`
        // will never return `None`, and `Some` is always returned.
        Some(current)
    }
}

// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
    Fibonacci { curr: 0, next: 1 }
}

fn main() {
    // `0..3` is an `Iterator` that generates: 0, 1, and 2.
    let mut sequence = (0_u8..3).into_iter();

    println!("Four consecutive `next` calls on 0..3");
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());

    // `for` works through an `Iterator` until it returns `None`.
    // Each `Some` value is unwrapped and bound to a variable (here, `i`).
    println!("Iterate through 0..3 using `for`");
    for i in 0_u8..3 {
        println!("> {}", i);
    }

    // The `take(n)` method reduces an `Iterator` to its first `n` terms.
    println!("The first four terms of the Fibonacci sequence are: ");
    for i in fibonacci().take(4) {
        println!("> {}", i);
    }

    // The `skip(n)` method shortens an `Iterator` by dropping its first `n` terms.
    //TODO: uncomment this once the corelib implements the `skip` method
    // println!("The next four terms of the Fibonacci sequence are: ");
    // for i in fibonacci().skip(4).take(4) {
    //     println!("> {}", i);
    // }

    let array = array![1_u32, 3, 3, 7];

    // The `iter` method produces an `Iterator` over an array/slice.
    println!("Iterate the following array {:?}", @array);
    let mut iterator = array.into_iter();
    for i in iterator {
        println!("> {}", i);
    }
}

Clone

When dealing with values in Cairo, the default behavior is to transfer ownership during assignments or function calls. However, sometimes we need to make a clone of the value as well.

The Clone trait helps us do exactly this. Most commonly, we can use the .clone() method defined by the Clone trait.

// An empty unit struct
#[derive(Drop, Copy, Debug)]
struct Unit {}

// A tuple struct that implements the Clone trait
#[derive(Clone, Drop, Debug)]
struct Pair {
    first: Box<u32>,
    second: Box<u32>,
}

fn main() {
    // Instantiate `Unit`
    let unit = Unit {};
    // Copy `Unit`, there are no resources to move
    let copied_unit = unit;

    // Both `Unit`s can be used independently
    println!("original: {:?}", unit);
    println!("copy: {:?}", copied_unit);

    // Instantiate `Pair`
    let pair = Pair { first: BoxTrait::new(1), second: BoxTrait::new(2) };
    println!("original: {:?}", pair);

    // Move `pair` into `moved_pair`
    let moved_pair = pair;
    println!("moved: {:?}", moved_pair);

    // Error! `pair` has been moved
    // println!("original: {:?}", pair);
    // TODO ^ Try uncommenting this line

    // Clone `moved_pair` into `cloned_pair` (resources are included)
    let cloned_pair = moved_pair.clone();

    // Drop the moved original by moving it into a function that takes ownership
    let _d = |_x: Pair| {};
    _d(moved_pair);

    // Error! `moved_pair` has been moved
    // println!("moved and dropped: {:?}", moved_pair);
    // TODO ^ Try uncommenting this line

    // The result from .clone() can still be used!
    println!("clone: {:?}", cloned_pair);
}

Disambiguating overlapping traits

A type can implement many different traits. What if two traits both require the same name for a function? For example, many traits might have a method named get(). They might even have different return types!

Good news: because each trait implementation gets its own impl block with a unique name, it's clear which trait's get method you're implementing.

What about when it comes time to call those methods? To disambiguate between them, we have to use Fully Qualified Syntax.

trait UsernameWidget<T> {
    // Get the selected username out of this widget
    fn get(self: @T) -> ByteArray;
}

trait AgeWidget<T> {
    // Get the selected age out of this widget
    fn get(self: @T) -> u8;
}

// A form with both a UsernameWidget and an AgeWidget
#[derive(Drop)]
struct Form {
    username: ByteArray,
    age: u8,
}

impl FormUsername of UsernameWidget<Form> {
    fn get(self: @Form) -> ByteArray {
        self.username.clone()
    }
}

impl FormAge of AgeWidget<Form> {
    fn get(self: @Form) -> u8 {
        *self.age
    }
}

fn main() {
    let form = Form { username: "caironaute", age: 28 };

    // If you uncomment this line, you'll get an error saying
    // "Ambiguous method call". Because, after all, there are multiple methods
    // named `get`.
    // println!("{}", form.get());

    let username = UsernameWidget::get(@form);
    assert!(username == "caironaute");
    let age = AgeWidget::get(@form);
    assert!(age == 28);
}

See also:

The Cairo Book chapter on Traits

Error handling

Error handling is the process of handling the possibility of failure. For example, failing to parse a number from a string and then continuing to use that bad input would clearly be problematic. Noticing and explicitly managing those errors saves the rest of the program from various pitfalls.

There are various ways to deal with errors in Cairo, which are described in the following subchapters. They all have more or less subtle differences and different use cases. As a rule of thumb:

An explicit panic is mainly useful for tests and dealing with unrecoverable errors, or to assert that a condition must never happens. For prototyping it can be useful, for example when dealing with functions that haven't been implemented yet. In tests panic is a reasonable way to explicitly fail.

The Option type is for when a value is optional or when the lack of a value is not an error condition. For attempting to convert a value V to a NonZero<V> - which would fail if said value is zero. When dealing with Options, unwrap is fine for prototyping and cases where it's absolutely certain that there is guaranteed to be a value. However expect is more useful since it lets you specify an error message in case something goes wrong anyway.

When there is a chance that things do go wrong and the caller has to deal with the problem, use Result. You can unwrap and expect them as well, but a proper error handling strategy is preferred.

For a more rigorous discussion of error handling, refer to the error handling section in the official book.

panic

The simplest error handling mechanism we will see is panic. It prints an error message, starts unwinding the stack, and exits the program. Here, we explicitly call panic on our error condition:

fn drink(beverage: ByteArray) {
    // You shouldn't drink too much sugary beverages.
    if beverage == "lemonade" {
        panic!("AAAaaaaa!!!!");
    }

    println!("Some refreshing {} is all I need.", beverage);
}

fn main() {
    drink("water");
    drink("lemonade");
    drink("still water");
}

The first call to drink works. The second panics and thus the third is never called.

Option & unwrap

In the last example, we showed that we can induce program failure at will. We told our program to panic if we drink a sugary lemonade. But what if we expect some drink but don't receive one? That case would be just as bad, so it needs to be handled!

We could test this against the null string ("") as we do with a lemonade. Since we're using Cairo, let's instead have the compiler point out cases where there's no drink.

An enum called Option<T> in the core library is used when absence is a possibility. It manifests itself as one of two "options":

  • Some(T): An element of type T was found
  • None: No element was found

These cases can either be explicitly handled via match or implicitly with unwrap. Implicit handling will either return the inner element or panic.

Note that it's possible to manually customize panic with expect, but unwrap otherwise leaves us with a less meaningful output than explicit handling. In the following example, explicit handling yields a more controlled result while retaining the option to panic if desired.

// The adult has seen it all, and can handle any drink well.
// All drinks are handled explicitly using `match`.
fn give_adult(drink: Option<ByteArray>) {
    // Specify a course of action for each case.
    match drink {
        Option::Some(inner) => println!("{}? How nice.", inner),
        Option::None => println!("No drink? Oh well."),
    }
}

// Others will `panic` before drinking sugary drinks.
// All drinks are handled implicitly using `unwrap`.
fn drink(drink: Option<ByteArray>) {
    // `unwrap` returns a `panic` when it receives a `None`.
    let inside = drink.unwrap();
    if inside == "lemonade" {
        panic!("AAAaaaaa!!!!")
    }

    println!("I love {}s!!!!!", inside);
}

fn main() {
    let water = Option::Some("water");
    let lemonade = Option::Some("lemonade");
    let void = Option::None;

    give_adult(water);
    give_adult(lemonade);
    give_adult(void);

    let coffee = Option::Some("coffee");
    let nothing = Option::None;

    drink(coffee);
    drink(nothing);
}

Unpacking options with ?

You can unpack Options by using match statements, but it's often easier to use the ? operator. If x is an Option, then evaluating x? will return the underlying value if x is Some, otherwise it will terminate whatever function is being executed and return None.

fn next_birthday(current_age: Option<u8>) -> Option<ByteArray> {
    // If `current_age` is `None`, this returns `None`.
    // If `current_age` is `Some`, the inner `u8` value + 1
    // gets assigned to `next_age`
    let next_age: u8 = current_age? + 1;
    Option::Some(format!("Next year I will be {}", next_age))
}

You can chain many ?s together to make your code much more readable.

#[derive(Copy, Drop)]
struct Person {
    job: Option<Job>,
}

#[derive(Drop, Clone, Copy)]
struct Job {
    phone_number: Option<PhoneNumber>,
}

#[derive(Drop, Clone, Copy)]
struct PhoneNumber {
    area_code: Option<u8>,
    number: u32,
}

#[generate_trait]
impl PersonImpl of PersonTrait {
    // Gets the area code of the phone number of the person's job, if it exists.
    fn work_phone_area_code(self: @Person) -> Option<u8> {
        // This would need many nested `match` statements without the `?` operator.
        // It would take a lot more code - try writing it yourself and see which
        // is easier.
        (*self).job?.phone_number?.area_code
    }
}

fn main() {
    let p = Person {
        job: Option::Some(
            Job {
                phone_number: Option::Some(
                    PhoneNumber { area_code: Option::Some(61), number: 439222222 },
                ),
            },
        ),
    };

    assert!(p.work_phone_area_code() == Option::Some(61));
}

Combinators: map

match is a valid method for handling Options. However, you may eventually find heavy usage tedious, especially with operations only valid with an input. In these cases, combinators can be used to manage control flow in a modular fashion.

Option has a built in method called map(), a combinator for the simple mapping of Some -> Some and None -> None. Multiple map() calls can be chained together for even more flexibility.

In the following example, process() replaces all functions previous to it while staying compact.

#[derive(Copy, Drop, Debug)]
enum Food {
    Apple,
    Carrot,
    Potato,
}

#[derive(Copy, Drop, Debug)]
struct Peeled {
    food: Food,
}

#[derive(Copy, Drop, Debug)]
struct Chopped {
    food: Food,
}

#[derive(Copy, Drop, Debug)]
struct Cooked {
    food: Food,
}

// Peeling food. If there isn't any, then return `None`.
// Otherwise, return the peeled food.
fn peel(food: Option<Food>) -> Option<Peeled> {
    match food {
        Option::Some(food) => Option::Some(Peeled { food }),
        Option::None => Option::None,
    }
}

// Chopping food. If there isn't any, then return `None`.
// Otherwise, return the chopped food.
fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
    match peeled {
        Option::Some(Peeled { food }) => Option::Some(Chopped { food }),
        Option::None => Option::None,
    }
}

// Cooking food. Here, we showcase `map()` instead of `match` for case handling.
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    chopped.map(|chopped: Chopped| Cooked { food: chopped.food })
}

// A function to peel, chop, and cook food all in sequence.
// We chain multiple uses of `map()` to simplify the code.
fn process(food: Option<Food>) -> Option<Cooked> {
    food
        .map(|f| Peeled { food: f })
        .map(|peeled| Chopped { food: peeled.food })
        .map(|chopped| Cooked { food: chopped.food })
}

// Check whether there's food or not before trying to eat it!
fn eat(food: Option<Cooked>) {
    match food {
        Option::Some(food) => println!("Mmm. I love {:?}", food),
        Option::None => println!("Oh no! It wasn't edible."),
    }
}

fn main() {
    let apple = Option::Some(Food::Apple);
    let carrot = Option::Some(Food::Carrot);
    let potato = Option::None;

    let cooked_apple = cook(chop(peel(apple)));
    let cooked_carrot = cook(chop(peel(carrot)));
    // Let's try the simpler looking `process()` now.
    let cooked_potato = process(potato);

    eat(cooked_apple);
    eat(cooked_carrot);
    eat(cooked_potato);
}

See also:

closures, Option, Option::map()

Combinators: and_then

map() was described as a chainable way to simplify match statements. However, using map() on a function that returns an Option<T> results in the nested Option<Option<T>>. Chaining multiple calls together can then become confusing. That's where another combinator called and_then(), known in some languages as flatmap, comes in.

and_then() calls its function input with the wrapped value and returns the result. If the Option is None, then it returns None instead.

In the following example, cookable_v3() results in an Option<Food>. Using map() instead of and_then() would have given an Option<Option<Food>>, which is an invalid type for eat().

#[derive(Copy, Drop, Debug)]
enum Food {
    CordonBleu,
    Steak,
    Sushi,
}

#[derive(Copy, Drop, Debug)]
enum Day {
    Monday,
    Tuesday,
    Wednesday,
}

// We don't have the ingredients to make Sushi.
fn have_ingredients(food: Food) -> Option<Food> {
    match food {
        Food::Sushi => Option::None,
        _ => Option::Some(food),
    }
}

// We have the recipe for everything except Cordon Bleu.
fn have_recipe(food: Food) -> Option<Food> {
    match food {
        Food::CordonBleu => Option::None,
        _ => Option::Some(food),
    }
}

// To make a dish, we need both the recipe and the ingredients.
// We can represent the logic with a chain of `match`es:
fn cookable_v1(food: Food) -> Option<Food> {
    match have_recipe(food) {
        Option::None => Option::None,
        Option::Some(food) => have_ingredients(food),
    }
}

// This can conveniently be rewritten more compactly with `and_then()`:
fn cookable_v3(food: Food) -> Option<Food> {
    have_recipe(food).and_then(|some_food| have_ingredients(some_food))
}

// Otherwise we'd need to `flatten()` an `Option<Option<Food>>`
// to get an `Option<Food>`:
fn cookable_v2(food: Food) -> Option<Food> {
    have_recipe(food).map(|some_food| have_ingredients(some_food)).flatten()
}

fn eat(food: Food, day: Day) {
    match cookable_v3(food) {
        Option::Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),
        Option::None => println!("Oh no. We don't get to eat on {:?}?", day),
    }
}

fn main() {
    let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);

    eat(cordon_bleu, Day::Monday);
    eat(steak, Day::Tuesday);
    eat(sushi, Day::Wednesday);
}

See also:

closures, Option, Option::and_then()

Unpacking options and defaults

There is more than one way to unpack an Option and fall back on a default if it is None. To choose the one that meets our needs, we need to consider the following:

  • do we need eager or lazy evaluation?
  • do we need to keep the original empty value intact, or modify it in place?

or() is chainable, evaluates eagerly, keeps empty value intact

or()is chainable and eagerly evaluates its argument, as is shown in the following example. Note that because or's arguments are evaluated eagerly, the variable passed to or is moved.

#[derive(Copy, Drop, Debug)]
enum Fruit {
    Apple,
    Orange,
    Banana,
    Kiwi,
    Lemon,
}

fn main() {
    let apple = Option::Some(Fruit::Apple);
    let orange = Option::Some(Fruit::Orange);
    let no_fruit: Option<Fruit> = Option::None;

    let first_available_fruit = no_fruit.or(orange).or(apple);
    println!("first_available_fruit: {:?}", first_available_fruit);
    // first_available_fruit: Some(Orange)

    // `or` moves its argument.
// In the example above, `or(orange)` returned a `Some`, so `or(apple)` was not invoked.
// But the variable named `apple` has been moved regardless, and cannot be used anymore.
// println!("Variable apple was moved, so this line won't compile: {:?}", apple);
// TODO: uncomment the line above to see the compiler error
}

or_else() is chainable, evaluates lazily, keeps empty value intact

Another alternative is to use or_else, which is also chainable, and evaluates lazily, as is shown in the following example:

#[derive(Copy, Drop, Debug)]
enum Fruit {
    Apple,
    Orange,
    Banana,
    Kiwi,
    Lemon,
}

fn main() {
    let no_fruit: Option<Fruit> = Option::None;
    let get_kiwi_as_fallback = 
        || {
            println!("Providing kiwi as fallback");
            Option::Some(Fruit::Kiwi)
        };
    let get_lemon_as_fallback = 
        || {
            println!("Providing lemon as fallback");
            Option::Some(Fruit::Lemon)
        };

    let first_available_fruit = no_fruit
        .or_else( || get_kiwi_as_fallback())
        .or_else( || get_lemon_as_fallback());
    println!("first_available_fruit: {:?}", first_available_fruit);
    // Providing kiwi as fallback
// first_available_fruit: Some(Kiwi)
}

See also:

closures, moved variables, or, or_else

Result

Result is a richer version of the Option type that describes possible error instead of possible absence.

That is, Result<T, E> could have one of two outcomes:

  • Ok(T): An element T was found
  • Err(E): An error was found with element E

By convention, the expected outcome is Ok while the unexpected outcome is Err.

Like Option, Result has many methods associated with it. unwrap(), for example, either yields the element T or panics. For case handling, there are many combinators between Result and Option that overlap.

In working with Cairo, you will likely encounter methods that return the Result type.

Let's see what happens when we successfully and unsuccessfully try to convert a character in a ByteArray to a number:

#[derive(Drop)]
struct ParseIntError {
    message: ByteArray,
}

fn char_to_number(c: ByteArray) -> Result<u8, ParseIntError> {
    if c.len() != 1 {
        return Result::Err(ParseIntError { message: "Expected a single character" });
    }
    let byte = c[0];
    Result::Ok(byte)
}

fn main() {
    let result = char_to_number("a");
    match result {
        Result::Ok(number) => println!("Number: 0x{:x}", number),
        Result::Err(error) => println!("Error: {}", error.message),
    }
    let result = char_to_number("ab");
    match result {
        Result::Ok(number) => println!("Number: 0x{:x}", number),
        Result::Err(error) => println!("Error: {}", error.message),
    }
}

In the unsuccessful case, char_to_number() returns an error for us to handle. We can improve the quality of our error message by being more specific about the return type and considering explicitly handling the error.

map for Result

Panicking in the previous example's multiply does not make for robust code. Generally, we want to return the error to the caller so it can decide what is the right way to respond to errors.

We first need to know what kind of error type we are dealing with. To determine the Err type, we look to our parse_ascii_digit function, which returns a ParseError type.

In the example below, the straightforward match statement leads to code that is overall more cumbersome.

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

// Helper function to parse a single ASCII digit from a ByteArray
fn parse_ascii_digit(value: ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

// With the return type rewritten, we use pattern matching without `unwrap()`.
fn multiply(first_number: ByteArray, second_number: ByteArray) -> Result<u32, ParseError> {
    match parse_ascii_digit(first_number) {
        Result::Ok(first_number) => {
            match parse_ascii_digit(second_number) {
                Result::Ok(second_number) => { Result::Ok(first_number * second_number) },
                Result::Err(e) => Result::Err(e),
            }
        },
        Result::Err(e) => Result::Err(e),
    }
}

fn print(result: Result<u32, ParseError>) {
    match result {
        Result::Ok(n) => println!("n is {}", n),
        Result::Err(e) => println!("Error: {}", e.message),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("4", "5");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);
}

Luckily, Option's map, and_then, and many other combinators are also implemented for Result. Result contains a complete listing.

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

// Helper function to parse a single ASCII digit from a ByteArray
fn parse_ascii_digit(value: ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

// As with `Option`, we can use combinators such as `map()`.
// This function is otherwise identical to the one above and reads:
// Multiply if both values can be parsed from ASCII digits, otherwise pass on the error.
fn multiply(first_number: ByteArray, second_number: ByteArray) -> Result<u32, ParseError> {
    parse_ascii_digit(first_number)
        .and_then(
            |first_number| {
                parse_ascii_digit(second_number).map(|second_number| first_number * second_number)
            },
        )
}

fn print(result: Result<u32, ParseError>) {
    match result {
        Result::Ok(n) => println!("n is {}", n),
        Result::Err(e) => println!("Error: {}", e.message),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("4", "5");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);
}

aliases for Result

How about when we want to reuse a specific Result type many times? Cairo allows us to create type aliases. Conveniently, we can define one for the specific Result in question.

At a module level, creating aliases can be particularly helpful. Errors found in a specific module often have the same Err type, so a single alias can succinctly define all associated Results.

Here's a quick example to show off the syntax:

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

// Helper function to parse a single ASCII digit from a ByteArray
fn parse_ascii_digit(value: ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

// Define a generic alias for a `Result` with the error type `ParseError`.
type AliasedResult<T> = Result<T, ParseError>;

// Use the above alias to refer to our specific `Result` type.
fn multiply(first_number: ByteArray, second_number: ByteArray) -> AliasedResult<u32> {
    parse_ascii_digit(first_number)
        .and_then(
            |first_number| {
                parse_ascii_digit(second_number).map(|second_number| first_number * second_number)
            },
        )
}

// Here, the alias again allows us to save some space.
fn print(result: AliasedResult<u32>) {
    match result {
        Result::Ok(n) => println!("n is {}", n),
        Result::Err(e) => println!("Error: {}", e.message),
    }
}

fn main() {
    print(multiply("4", "5"));
    print(multiply("t", "2"));
}

See also:

Early returns

In the previous example, we explicitly handled the errors using combinators. Another way to deal with this case analysis is to use a combination of match statements and early returns.

That is, we can simply stop executing the function and return the error if one occurs. For some, this form of code can be easier to both read and write. Consider this version of the previous example, rewritten using early returns:

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

// Helper function to parse a single ASCII digit from a ByteArray
fn parse_ascii_digit(value: ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

fn multiply(first_number: ByteArray, second_number: ByteArray) -> Result<u32, ParseError> {
    let first_number = match parse_ascii_digit(first_number) {
        Result::Ok(first_number) => first_number,
        Result::Err(e) => { return Result::Err(e); },
    };

    let second_number = match parse_ascii_digit(second_number) {
        Result::Ok(second_number) => second_number,
        Result::Err(e) => { return Result::Err(e); },
    };

    Result::Ok(first_number * second_number)
}

fn print(result: Result<u32, ParseError>) {
    match result {
        Result::Ok(n) => println!("n is {}", n),
        Result::Err(e) => println!("Error: {}", e.message),
    }
}

fn main() {
    print(multiply("4", "5"));
    print(multiply("t", "2"));
}

At this point, we've learned to explicitly handle errors using combinators and early returns. While we generally want to avoid panicking, explicitly handling all of our errors is cumbersome.

In the next section, we'll introduce ? for the cases where we simply need to unwrap without possibly inducing panic.

Introducing ?

Sometimes we just want the simplicity of unwrap without the possibility of a panic. Until now, unwrap has forced us to nest deeper and deeper when what we really wanted was to get the variable out. This is exactly the purpose of ?.

Upon finding an Err, there are two valid actions to take:

  1. panic! which we already decided to try to avoid if possible
  2. return because an Err means it cannot be handled

? is almost[^†] exactly equivalent to an unwrap which returns instead of panicking on Errs. Let's see how we can simplify the earlier example that used combinators:

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

// Helper function to parse a single ASCII digit from a ByteArray
fn parse_ascii_digit(value: ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

fn multiply(first_number: ByteArray, second_number: ByteArray) -> Result<u32, ParseError> {
    let first_number = parse_ascii_digit(first_number)?;
    let second_number = parse_ascii_digit(second_number)?;

    Result::Ok(first_number * second_number)
}

fn print(result: Result<u32, ParseError>) {
    match result {
        Result::Ok(n) => println!("n is {}", n),
        Result::Err(e) => println!("Error: {}", e.message),
    }
}

fn main() {
    print(multiply("4", "5"));
    print(multiply("t", "2"));
}

Multiple error types

The previous examples have always been very convenient; Results interact with other Results and Options interact with other Options.

Sometimes an Option needs to interact with a Result, or a Result<T, Error1> needs to interact with a Result<T, Error2>. In those cases, we want to manage our different error types in a way that makes them composable and easy to interact with.

In the following code, two instances of unwrap generate different error types. array.get() returns an Option, while our parse_ascii_digit returns a Result<u32, ParseError>:

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

fn parse_ascii_digit(value: @ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

fn double_first(arr: Array<ByteArray>) -> u32 {
    let first = arr[0]; // Generate error 1
    2 * parse_ascii_digit(first).unwrap() // Generate error 2
}

fn main() {
    let numbers = array!["4", "9", "1"];
    let empty = array![];
    let strings = array!["t", "9", "1"];

    println!("The first doubled is {}", double_first(numbers));

    println!("The first doubled is {}", double_first(empty));
    // Error 1: the array is empty

    println!("The first doubled is {}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

Over the next sections, we'll see several strategies for handling these kind of problems.

Pulling Results out of Options

The most basic way of handling mixed error types is to just embed them in each other.

#[derive(Drop, Debug)]
struct ParseError {
    message: ByteArray,
}

fn parse_ascii_digit(value: @ByteArray) -> Result<u32, ParseError> {
    if value.len() != 1 {
        Result::Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Result::Ok((byte - '0').into())
        } else {
            Result::Err(ParseError { message: "Character is not a digit" })
        }
    }
}

fn double_first(arr: Array<ByteArray>) -> Option<Result<u32, ParseError>> {
    arr.get(0).map(|first| {
        parse_ascii_digit(first.unbox()).map(|n| 2 * n)
    })
}

fn main() {
    let numbers = array!["4", "9", "1"];
    let empty = array![];
    let strings = array!["t", "9", "1"];

    println!("The first doubled is {:?}", double_first(numbers));

    println!("The first doubled is {:?}", double_first(empty));
    // Error 1: the array is empty

    println!("The first doubled is {:?}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

Defining an error type

Sometimes it simplifies the code to mask all of the different errors with a single type of error. We'll show this with a custom error.

Cairo allows us to define our own error types. In general, a "good" error type:

  • Represents different errors with the same type
  • Presents nice error messages to the user
  • Is easy to compare with other types
    • Good: Result::Err(EmptyArray)
    • Bad: Result::Err("Please use an array with at least one element")
  • Can hold information about the error
    • Good: Result::Err(BadChar(c, position))
    • Bad: Result::Err("+ cannot be used here")
  • Composes well with other errors
type Result<T> = core::result::Result<T, DoubleError>;

// Define our error types. These may be customized for our error handling cases.
// Now we will be able to write our own errors, defer to an underlying error
// implementation, or do something in between.
#[derive(Drop, Debug)]
enum DoubleError {
    EmptyArray,
    Parse,
}

impl DoubleErrorImpl of core::fmt::Display<DoubleError> {
    fn fmt(
        self: @DoubleError, ref f: core::fmt::Formatter,
    ) -> core::result::Result<(), core::fmt::Error> {
        match self {
            DoubleError::EmptyArray => write!(f, "please use an array with at least one element"),
            DoubleError::Parse => write!(f, "invalid digit to double"),
        }
    }
}

#[derive(Drop)]
struct ParseError {
    message: ByteArray,
}

fn parse_ascii_digit(value: @ByteArray) -> core::result::Result<u32, ParseError> {
    if value.len() != 1 {
        Err(ParseError { message: "Expected a single character" })
    } else {
        let byte = value[0];
        if byte >= '0' && byte <= '9' {
            Ok((byte - '0').into())
        } else {
            Err(ParseError { message: "Character is not a digit" })
        }
    }
}

fn double_first(arr: Array<ByteArray>) -> Result<u32> {
    arr
        .get(0)
        // Change the error to our new type.
        .ok_or(DoubleError::EmptyArray)
        .and_then(
            |s| {
                parse_ascii_digit(s.unbox()) // Update to the new error type here also.
                    .map_err(|_err| DoubleError::Parse)
                    .map(|i| 2 * i)
            },
        )
}

fn print(result: Result<u32>) {
    match result {
        Ok(n) => println!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    let numbers = array!["4", "9", "1"];
    let empty = array![];
    let strings = array!["t", "9", "1"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Core library types

The core library provides many custom types which expands drastically on the primitives. Some of these include:

  • growable ByteArrays like: "hello world"
  • growable arrays: array![1, 2, 3]
  • optional types: Option<i32>
  • error handling types: Result<i32, i32>
  • smart pointers: Box<i32>

See also:

primitives and the core library

Dictionaries

Where arrays store values in a contiguous segment of memory at an index relative to the start of the array, Felt252Dicts store values by key. Cairo's Felt252Dict uses felt252 as the key type, which can represent integers, short strings (31 characters or less), or other values that can be converted to felt252.

Felt252Dicts are efficient key-value storage structures in Cairo that create new entries for each operation. When a dictionary goes out of scope, it must be squashed to validate all operations. You can create a dictionary using Default::default().

use core::dict::Felt252Dict;

fn call(number: felt252) -> ByteArray {
    if number == 7981364 {
        return "We're sorry, the call cannot be completed as dialed. Please hang up and try again.";
    } else if number == 6457689 {
        return "Hello, this is Mr. Awesome's Pizza. My name is Fred. What can I get for you today?";
    } else {
        return "Hi! Who is this again?";
    }
}

fn main() {
    let mut contacts: Felt252Dict<felt252> = Default::default();

    // Insert values for different keys
    contacts.insert('Daniel', 7981364);
    contacts.insert('Ashley', 6457689);
    contacts.insert('Katie', 4358291);
    contacts.insert('Robert', 9561745);

    // Get a value and match on it
    let number = contacts.get('Daniel');
    println!("{}", call(number));

    // Insert a new value for an existing key
    contacts.insert('Daniel', 1646743);

    // Keys not in the dict return the default value - here, 0
    let number = contacts.get('Brandon');
    println!("Brandon's number is {}", number);
}

Box and Memory Segments

Values in Cairo are stored in a memory segment called the execution segment by default. Values can be boxed (allocated in the boxed segment) by creating a Box<T>. A box is a smart pointer that provides a way to store a value of type T in Cairo VM's boxed segment, leaving only a pointer in the execution segment.

The main purposes of boxes are to:

  • Store values of arbitrary size while maintaining a fixed-size pointer
  • Enable recursive types that would otherwise have infinite size
  • Move large data structures efficiently by passing pointers instead of copying values

Boxed values can be accessed using the unbox() method or through the Deref trait, which retrieves the value from the boxed segment.

#[derive(Copy, Drop, Debug)]
struct Point {
    x: u128,
    y: u128,
}

// A Rectangle can be specified by where its top left and bottom right
// corners are in space
#[derive(Copy, Drop, Debug)]
struct Rectangle {
    top_left: Point,
    bottom_right: Point,
}

fn origin() -> Point {
    Point { x: 0, y: 0 }
}

fn boxed_origin() -> Box<Point> {
    // Allocate this point in the boxed segment, and return a pointer to it
    BoxTrait::new(Point { x: 0, y: 0 })
}

fn main() {
    // Values in execution segment
    let point: Point = origin();
    let _rectangle: Rectangle = Rectangle {
        top_left: origin(), bottom_right: Point { x: 3, y: 4 },
    };

    // Boxed segment allocated rectangle
    let boxed_rectangle: Box<Rectangle> = BoxTrait::new(
        Rectangle { top_left: origin(), bottom_right: Point { x: 3, y: 4 } },
    );

    // The output of functions can be boxed
    let boxed_point: Box<Point> = BoxTrait::new(origin());

    // Double indirection
    let _box_in_a_box: Box<Box<Point>> = BoxTrait::new(boxed_origin());

    // Two ways to access boxed values:
    // 1. Using unbox()
    let unboxed_point: Point = boxed_point.unbox();
    // 2. Using deref() trait
    let _derefed_point: Point = boxed_point.deref();

    // Print some values to demonstrate
    println!("point: {:?}", point);
    println!("unboxed_point: {:?}", unboxed_point);
    println!("boxed_rectangle: {:?}", boxed_rectangle.unbox().top_left);
}

Working with Recursive Types

One of the most important use cases for Box<T> is enabling recursive types. Without boxing, recursive types would have infinite size - as demonstrated in the Linked List example.

ByteArrays

The main string type in Cairo is ByteArray, which is an optimized data structure to store sequences of bytes. This is mainly used for strings, but can also be used to hold any sequence of bytes.

fn main() {
    let pangram: ByteArray = "the quick brown fox jumps over the lazy dog";
    println!("Pangram: {}", pangram);

    // This is a simplified example showing iteration
    let mut chars = pangram.clone().into_iter();
    for c in chars {
        println!("ASCII: 0x{:x}", c);
    }

    // ByteArray can be concatenated using the + operator
    let alice: ByteArray = "I like dogs";
    let bob: ByteArray = "I like " + "cats";

    println!("Alice says: {}", alice);
    println!("Bob says: {}", bob);
}

More ByteArray methods can be found under the core::byte_array module.

Option

Sometimes it's desirable to catch the failure of some parts of a program instead of calling panic!; this can be accomplished using the Option enum.

The Option<T> enum has two variants:

  • None, to indicate failure or lack of value, and
  • Some(value), a tuple struct that wraps a value with type T.
// An integer division that doesn't `panic!`
fn checked_division(dividend: u32, divisor: u32) -> Option<u32> {
    if divisor == 0 {
        // Failure is represented as the `None` variant
        None
    } else {
        // Result is wrapped in a `Some` variant
        Some(dividend / divisor)
    }
}

// This function handles a division that may not succeed
fn try_division(dividend: u32, divisor: u32) {
    // `Option` values can be pattern matched, just like other enums
    match checked_division(dividend, divisor) {
        None => println!("{} / {} failed!", dividend, divisor),
        Some(quotient) => { println!("{} / {} = {}", dividend, divisor, quotient) },
    }
}

fn main() {
    try_division(4, 2);
    try_division(1, 0);

    // Binding `None` to a variable needs to be type annotated
    let none: Option<u32> = None;
    let _equivalent_none = None::<u32>;

    let optional_float = Some(0);

    // Unwrapping a `Some` variant will extract the value wrapped.
    println!("{:?} unwraps to {:?}", optional_float, optional_float.unwrap());

    // Unwrapping a `None` variant will `panic!`
    println!("{:?} unwraps to {:?}", none, none.unwrap());
}

Result

We've seen that the Option enum can be used as a return value from functions that may fail, where None can be returned to indicate failure. However, sometimes it is important to express why an operation failed. To do this we have the Result enum.

The Result<T, E> enum has two variants:

  • Ok(value) which indicates that the operation succeeded, and wraps the value returned by the operation. (value has type T)
  • Err(why), which indicates that the operation failed, and wraps why, which (hopefully) explains the cause of the failure. (why has type E)
mod checked {
    use core::num::traits::Sqrt;

    // Mathematical "errors" we want to catch
    #[derive(Drop, Debug)]
    enum MathError {
        DivisionByZero,
        NegativeSquareRoot,
    }

    type MathResult = Result<u32, MathError>;

    pub fn div(x: u32, y: u32) -> MathResult {
        if y == 0 {
            // This operation would `fail`, instead let's return the reason of
            // the failure wrapped in `Err`
            Err(MathError::DivisionByZero)
        } else {
            // This operation is valid, return the result wrapped in `Ok`
            Ok(x / y)
        }
    }

    pub fn sqrt(x: u32) -> MathResult {
        match x {
            0 => Ok(0),
            _ => Ok(x.sqrt().into()),
        }
    }
}

// `op(x, y)` === `sqrt(x / y)`
pub fn op(x: u32, y: u32) -> u32 {
    // This is a two level match pyramid!
    match checked::div(x, y) {
        Err(why) => panic!("{:?}", why),
        Ok(ratio) => match checked::sqrt(ratio) {
            Err(why) => panic!("{:?}", why),
            Ok(sqrt) => sqrt,
        },
    }
}

pub fn main() {
    // Will this fail?
    println!("{}", op(1, 10));
}

?

Chaining results using match can get pretty untidy; luckily, the ? operator can be used to make things pretty again. ? is used at the end of an expression returning a Result, and is equivalent to a match expression, where the Err(err) branch expands to an early return Err(err), and the Ok(ok) branch expands to an ok expression.

mod checked {
    use core::num::traits::Sqrt;

    #[derive(Drop)]
    pub enum MathError {
        DivisionByZero,
        NegativeSquareRoot,
    }

    pub type MathResult = Result<u32, MathError>;

    pub fn div(x: u32, y: u32) -> MathResult {
        if y == 0 {
            Err(MathError::DivisionByZero)
        } else {
            Ok(x / y)
        }
    }

    pub fn sqrt(x: u32) -> MathResult {
        match x {
            0 => Ok(0),
            _ => Ok(x.sqrt().into()),
        }
    }

    // Intermediate function
    pub fn op_(x: u32, y: u32) -> MathResult {
        // if `div` "fails", then `DivisionByZero` will be `return`ed
        let ratio = div(x, y)?;

        // if `sqrt` "fails", then `NegativeSquareRoot` will be `return`ed
        sqrt(ratio)
    }

    pub fn op(x: u32, y: u32) {
        match op_(x, y) {
            Err(why) => panic!(
                "{:?}",
                match why {
                    MathError::DivisionByZero => println!("division by zero"),
                    MathError::NegativeSquareRoot => println!("square root of negative number"),
                },
            ),
            Ok(value) => println!("{}", value),
        }
    }
}

fn main() {
    checked::op(1, 10);
}

Be sure to check the documentation, as there are many methods to map/compose Result.

panic!

The panic! macro can be used to generate a panic and start unwinding its stack. While unwinding, the runtime will take care of freeing all the resources owned by the thread by calling the destructor of all its objects.

Since we are dealing with programs with only one thread, panic! will cause the program to report the panic message and exit.

// Re-implementation of integer division (/)
fn division(dividend: i32, divisor: i32) -> i32 {
    if divisor == 0 {
        // Division by zero triggers a panic
        panic!("division by zero")
    } else {
        dividend / divisor
    }
}

// The `main` task
fn main() {
    // This operation will trigger a failure
    division(3, 0);

    println!("This point won't be reached!");
}

Testing

Cairo is a programming language that cares a lot about correctness and it includes support for writing software tests within the language itself through two testing frameworks: Cairo Test and Starknet Foundry. While Starknet Foundry is principally aimed at Starknet development, it presents useful features for pure Cairo development as well, which makes it the preferred choice for most Cairo developers. The examples presented here will use Starknet Foundry.

Testing comes in two styles:

Also, Cairo has support for specifying additional dependencies for tests:

See Also

Unit testing

Tests are Cairo functions that verify that the non-test code is functioning in the expected manner. The bodies of test functions typically perform some setup, run the code we want to test, then assert whether the results are what we expect.

Most unit tests go into a tests mod with the #[cfg(test)] attribute. Test functions are marked with the #[test] attribute.

Tests fail when something in the test function panics. There are some helper macros:

  • assert!(expression) - panics if expression evaluates to false.
  • assert_eq!(left, right) and assert_ne!(left, right) - testing left and right expressions for equality.
  • assert_lt!(left, right) and assert_gt!(left, right) - testing left and right expressions for less than and greater than respectively.
  • assert_le!(left, right) and assert_ge!(left, right) - testing left and right expressions for less than or equal to and greater than or equal to respectively.
// Basic add example
fn add(a: u32, b: u32) -> u32 {
    a + b
}

// This is a really bad adding function, its purpose is to fail in this
// example.
fn bad_add(a: u32, b: u32) -> u32 {
    a - b
}

#[cfg(test)]
mod add_tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        // This assert would fire and test will fail.
        // Please note, that private functions can be tested too!
        assert_eq!(bad_add(1, 2), 3);
    }
}

Tests can be run with scarb test. To run specific tests, one may specify the test name to scarb test command. To run multiple tests one may specify part of a test name that matches all the tests that should be run. Here, we run all the tests in the add_tests module.

$ scarb test add
     Running test unit_testing (snforge test)
    Blocking waiting for file lock on registry db cache
   Compiling test(listings/testing/unit_testing/Scarb.toml)
    Finished `dev` profile target(s) in 6 seconds


Collected 4 test(s) from unit_testing package
Running 4 test(s) from src/
[PASS] unit_testing::add_tests::test_add (gas: ~1)
[PASS] unit_testing::ignore_tests::test_add (gas: ~1)
[FAIL] unit_testing::add_tests::test_bad_add

Failure data:
    0x7533325f737562204f766572666c6f77 ('u32_sub Overflow')

[PASS] unit_testing::ignore_tests::test_add_hundred (gas: ~1)
Tests: 3 passed, 1 failed, 0 skipped, 0 ignored, 4 filtered out

Failures:
    unit_testing::add_tests::test_bad_add

Testing panics

To check functions that should panic under certain circumstances, use attribute #[should_panic]. This attribute accepts optional parameter expected: with the text of the panic message. If your function can panic in multiple ways, it helps make sure your test is testing the correct panic.

fn divide_non_zero_result(a: u32, b: u32) -> u32 {
    if b == 0 {
        panic!("Divide-by-zero error")
    } else if a < b {
        panic!("Divide result is zero")
    }
    a / b
}

#[cfg(test)]
mod divide_tests {
    use super::*;

    #[test]
    fn test_divide() {
        assert_eq!(divide_non_zero_result(10, 2), 5);
    }

    #[test]
    #[should_panic]
    fn test_any_panic() {
        divide_non_zero_result(1, 0);
    }

    #[test]
    #[should_panic(expected: "Divide result is zero")]
    fn test_specific_panic() {
        divide_non_zero_result(1, 10);
    }
}

Running these tests gives us:

$ scarb test divide
     Running test unit_testing (snforge test)
   Compiling test(listings/testing/unit_testing/Scarb.toml)
    Finished `dev` profile target(s) in 6 seconds


Collected 3 test(s) from unit_testing package
Running 3 test(s) from src/
[PASS] unit_testing::divide_tests::test_divide (gas: ~1)
[PASS] unit_testing::divide_tests::test_any_panic (gas: ~1)

Success data:
    "Divide-by-zero error"

[PASS] unit_testing::divide_tests::test_specific_panic (gas: ~1)
Tests: 3 passed, 0 failed, 0 skipped, 0 ignored, 5 filtered out

Ignoring tests

Tests can be marked with the #[ignore] attribute to exclude some tests. Ignored tests can be run with command scarb test -- --ignored

fn add_two(a: u32, b: u32) -> u32 {
    a + b
}

#[cfg(test)]
mod ignore_tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add_two(2, 2), 4);
    }

    #[test]
    fn test_add_hundred() {
        assert_eq!(add_two(100, 2), 102);
        assert_eq!(add_two(2, 100), 102);
    }

    #[test]
    #[ignore]
    fn ignored_test() {
        assert_eq!(add_two(0, 0), 0);
    }
}
$ scarb test -- --ignored
     Running test unit_testing (snforge test)
   Compiling test(listings/testing/unit_testing/Scarb.toml)
    Finished `dev` profile target(s) in 5 seconds


Collected 1 test(s) from unit_testing package
Running 1 test(s) from src/
[PASS] unit_testing::ignore_tests::ignored_test (gas: ~1)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 7 filtered out

Integration testing

Unit tests are testing one module in isolation at a time: they're small and can test private code. Integration tests are external to your crate and use only its public interface in the same way any other code would. Their purpose is to test that many parts of your library work correctly together.

Scarb looks for integration tests in tests directory next to src.

File src/lib.cairo:

// Define this in a crate called `adder`.
pub fn add(a: u32, b: u32) -> u32 {
    a + b
}

File with test: tests/integration_test.cairo:

#[test]
fn test_add() {
    assert(adder::add(3, 2) == 5, 'addition failed');
}

Running tests with scarb test command:

$ scarb test 
     Running test adder (snforge test)
    Blocking waiting for file lock on registry db cache
    Blocking waiting for file lock on registry db cache
   Compiling test(listings/testing/integration_testing/Scarb.toml)
   Compiling test(listings/testing/integration_testing/Scarb.toml)
    Finished `dev` profile target(s) in 11 seconds


Collected 1 test(s) from adder package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] adder_integrationtest::integration_test::test_add (gas: ~1)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

If the tests directory does not contain a lib.cairo file, each Cairo source file in the tests directory is compiled as a separate crate. In order to share some code between integration tests we can define a lib.cairo file in the tests directory, which will create a single target named {package_name}_tests, and use its contents within tests by importing it.

File tests/common.cairo:

pub fn setup() { // some setup code, like creating required variables, etc.
}

File with test: tests/lib.cairo

// importing common module.
mod common;

#[test]
fn test_add() {
    // using common code.
    common::setup();
    assert(adder::add(3, 2) == 5, 'addition failed');
}

Creating the module as tests/lib.cairo is recommended to make the tests directory behave like a regular crate, avoiding each file being compiled as a separate test crate.

Development dependencies

Sometimes there is a need to have dependencies for tests (or examples, or benchmarks) only. Such dependencies are added to Scarb.toml in the [dev-dependencies] section. These dependencies are not propagated to other packages which depend on this package. When you start a new project with scarb new, you will notice that the Scarb.toml file contains the assert_macros dev-dependency. This macro is required to use the assert_ macros presented in unit-testing.

File Scarb.toml:

[package]
name = "adder"
version = "0.1.0"
edition = "2024_07"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet = "2.9.2"

File src/lib.cairo:

fn add(a: u32, b: u32) -> u32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::add;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

See Also

Scarb docs on specifying dependencies.

Meta

Some topics aren't exactly relevant to how your program runs but provide you tooling or infrastructure support which just makes things better for everyone. These topics include:

  • Documentation: Generate library documentation for users via the included scarb doc.
  • Playground: Analyze your Cairo code in the Cairo Playground.

Documentation

Use scarb doc to build documentation in target/doc, and running mdbook serve in the output directory will automatically open it in your web browser.

Doc comments

Doc comments are very useful for big projects that require documentation. When running scarb doc, these are the comments that get compiled into documentation. They are denoted by a ///, and support Markdown.

/// A human being is represented here
#[derive(Drop)]
pub struct Person {
    /// A person must have a name, no matter how much Juliet may hate it
    name: ByteArray,
}

#[generate_trait]
impl PersonImpl of PersonTrait {
    /// Creates a person with the given name.
    ///
    /// # Examples
    ///
    /// ```
    /// // You can have cairo code between fences inside the comments
    /// use doc::Person;
    /// let person = PersonTrait::new("name");
    /// ```
    fn new(name: ByteArray) -> Person {
        Person { name: name }
    }

    /// Gives a friendly hello!
    ///
    /// Says "Hello, [name](Person::name)" to the `Person` it is called on.
    fn hello(self: @Person) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = PersonTrait::new("John");

    john.hello();
}

For documentation, scarb doc is widely used by the community. It's what is used to generate the core library docs.

See also:

Playground

The Cairo Playground is a way to experiment with Cairo code through a web interface.

You can compile your program to Sierra and CASM, and execute it instruction-by-instruction.