Cairo v2.5.0 is out!

Cairo v2.5.0

TL;DR

Cairo 2.5.0 was just released. This version only involves high-level compiler (Cairo→Sierra) changes and thus can be used to deploy contracts on Starknet Testnet and Mainnet without delay. This version introduces various features to the language: removing many of the limitations existing in match expressions, introducing friendlier loop syntax, adding the notion of pub and more. Continue with the rest of the post to see what’s new in this version in more detail.

match expression Improvements

Matching over enums

Cairo 2.5.0 improves matching over enums as follows:

  • A match arm for every variant is no longer needed, and the “otherwise” _ => { ... } can be used when matching over enums
  • Order of variants is no longer enforced to be the same as in the enum definition
  • Or expressions between variants are now supported
  • Matching over a tuple of enums is now supported
enum Color {
    Red,
    Green,
    Blue,
    Yellow
}

  enum Weekend {
      Friday,
      Saturday,
      Sunday
  }

  fn foo(c: Color) {
      match c {
          Color::Yellow => {},
          Color::Green | Color::Blue => {},
          _ => {}
      }
  }

  fn bar(t: (Color, Weekend)) -> u16 {
      match t {
          (Color::Red, Weekend::Friday) => 11,
          (_, Weekend::Saturday) => 12,
          (_, _) => 13
      }
  }

Matching over integers

With Cairo 2.5.0 we can match over integers (signed or unsigned).

fn foo(a: u16) -> u16 {
    match a {
        0 | 1 => 11,
        2 | 3 => 12,
        4 | _ => 13,
    }
}

The supported types are ints/uints that fit inside a single felt252, that is, not including u256. Note that match expressions over felts/integers must still be over a sequential segment starting at zero; this restriction will be relaxed in future versions.

while loops

While loops are now part of Cairo and join the loop { ... } construct that used to be the only way to do loops so far.

let mut i: u8 = 0;
while i < 10 {
    println!("{i}");
    i = i + 1;
}

Storing strings

ByteArrays were added to the language in the last version, 2.4.0, but were still not storable within a contract. In this version we added an implementation of Store<ByteArray> to the corelib.

Layout in storage

Consider the following contract:

#[starknet::contract]
mod MyContract {
    #[storage]
    struct Storage {
        url: ByteArray
    }

    #[generate_trait]
    #[abi(per_item)]
    impl MyContractImpl of MyContractTrait {
        #[external(v0)]
        fn get_url(self: @ContractState) -> ByteArray {
            self.url.read()
        }

        #[external(v0)]
        fn set_url(ref self: ContractState, new_url: ByteArray) {
            self.url.write(new_url);
        }
    }
}

When we set the url, its length in bytes will be stored in the base address of the url storage variable, sn_keccak("url"). The data is divided into chunks of 256 felts, each containing exactly 31bytes, and a single felt remainder which contains at most 30bytes. Each chunk is stored continuously, and the starting address of the i’th chunk is hades_permutation(address, i, "ByteArray"), where “ByteArray” is the felt252 representing the ASCII encoding of the fixed string “ByteArray”, and serves the purpose of domain separation between byte array addresses and other hashes in the system.

For example, strings of less than 32 characters such as “hello world” will require two storage slots, one for the length in bytes, and another single felt252 representing the ASCII encoding of “hello world”. In the “hello world” example, the value in Starknet’s storage at sn_keccak("url") will be 11 (length), and the data will be stored at hades_permuatation(sn_keccak("url"), 0, "ByteArray").

For longer strings that are up to 256*31 characters, the i'th 31 bytes word will be stored at hades_permuatation(sn_keccak("url"), 0, "ByteArray")+i. In general, the i'th 31-byte word of a string is stored at hades_permutation(base_address, i/(256*31), "ByteArray")+ i mod (256*31).

The pub keyword

Up until now, all the definitions from all of our package’s dependencies were visible. We’re now introducing the following keywords:

  • pub - a definition marked with pub in package A will be visible whenever its imported from A (with an appropriate use statement)
  • pub(crate) - a definition marked with pub(crate) is (analogously to rust) only visible within the package in which the definition is included.

pub and pub(crate) are applicable to the following:

  • modules
  • structs and struct members
  • enums
  • consts
  • functions
  • traits and impls

Whenever a trait is pub, the inference mechanism for finding an appropriate impl (i.e. the mechanism that resolves function calls with the dot operator) will find appropriate impls regardless of the impls visibility. Writing pub impl only allows us to access the impl path externally. Note that we cannot distinguish between the visibility of individual functions within traits or impls.

This is a breaking change, hence it is rolled out in a new edition, 2023_11. Note that if I have a dependency D, which released a new version using edition 2023_11, I will not be able to import none pub definitions from the latest version of D. A similar change took place in the corelib itself, and some of the definitions are no longer public. It is advised to update your code to use the latest edition rather than deepen your dependency on corelib definitions that are not expected to be available in the long term.

Experimental features - negative impls

You can now add an experimental_features field to your Scarb.toml. Experimental features may be unsupported or changed in future compiler versions, hence they are only allowed under a special configuration flag.

Currently, we have one experimental feature that is added in Cairo v2.5.0:

[package]
name = "foobar"
experimental-features = ["negative_impls"]

Negative impls

Negative impls are a way to write implementations that are applicable only in case another implementation does not exist in the current scope.

The motivating example is being able to write Option<Felt252Dict<T>>. Today, the following code:

let dict: Option<Felt252Dict<u32>> = Option::Some(Default::default());

runs into the following errors (since every type must be either droppable or destructible):

note: Trait has no implementation in context: core::traits::Drop::<core::option::Option::<core::dict::Felt252Dict::<core::integer::u32>>>

note: Trait has no implementation in context: core::traits::Destruct::<core::option::Option::<core::dict::Felt252Dict::<core::integer::u32>>>

We can try to solve the above by adding the following impl to the corelib:

impl DestructOption<T, +Destruct<T>> of Destruct<Option<T>> {
    fn destruct(self: Option<T>) nopanic {
        match self {
            Option::Some(value) => value.destruct(),
            Option::None => {}
        }
    }
}

The problem now is that calling destruct on Option<T> where T is droppable yields a collision between two impls: our new OptionDestruct and the existing DestructFromDrop:

impl DestructFromDrop<T, +Drop<T>> of Destruct<T> {
    #[inline(always)]
    fn destruct(self: T) nopanic {}
}

To overcome this, we change the definition of our new DestructOption so it will only be applicable for types T that are not droppable:

pub impl DestructOption<T, +Destruct<T>, -Drop<Option<T>>> of Destruct<Option<T>>

The -Drop<Option<T>> addition tells us that the DestructOption impl is only available for types for which an impl of Drop<Option<T>> is not found, hence we avoid the collision. In general, negative impls allow us to control when our new impl is available.

Compiler warnings

The compiler now outputs various warnings. Below we list a few common examples.

Unused variables:

fn foo() {
    let x = bar();
}

Unused variables, such as in the above example, will produce the following warning:

warn: Unused variable. Consider ignoring by prefixing with `_`.

Unhandled result:

The line replace_class_syscall(123.try_into().unwrap()) yields the following warning, which will be removed .unwrap_syscall() to handle the syscall result:

warn: Unhandled `#[must_use]` type `core::result::Result::<(), core::array::Array::<core::felt252>>`

Safe dispatchers

Recall that when you’re defining a Starknet interface then corresponding dispatcher types are generated by the compiler. For example, if we write the following:

#[starknet::interface]
trait IOtherContract<TContractState> {
    fn some_function(self: @TContractState) -> u128;
}

then the compiler generates:

  • IOtherContractDispatcher
  • OtherContractLibraryDispatcher
  • IOtherContractSafeDispatcher
  • IOtherContractSafeLibraryDispatcher

All the above are used to conveniently call some_function in another contract, for example:

let addr = ...
let other_contract = IOtherContractDispatcher { contract_address: addr }
let res: u128 = other_contract.some_function();

The difference between the regular dispatchers and safe dispatchers, is that regular dispatchers call unwrap_syscall for you (hence clearly panic if the contract call fails), while safe dispatchers are returning SyscallResult.

While the Cairo test runner propagates errors to the calling contract when safe dispatchers are used, the non-panicking behavior will not be observed on Starknet itself! The production systems (Starknet Testnet or Mainnet) do not yet support graceful failure in internal calls. If an inner call panics, the entire transaction immediately reverts. This will change in the future, and no change to the contract’s code is required; that is, if you used safe dispatchers, once support for graceful failures in inner calls is added to the Starknet OS, your deployed contracts will immediately start to behave as expected (allowing the caller to handle the error). For now, note that you cannot yet rely on safe dispatchers not panicking in production.

Due to the above, as of Cairo v2.5.0, calling safe dispatcher functions will result in the following warning:

Usage of unstable feature safe_dispatcher with no `#[feature({safe_dispatcher})]` attribute

To ignore the warning, you should add the #[feature(safe_dispatcher)] attribute above calls to safe dispatcher functions (note again that the behavior in Starknet today is inconsistent with the expected behavior of safe dispatchers).

Amazing features! Is there any plan to add support of for loops, if let, while let and let else control flows?

iterators and for loops definitely are part of the roadmap, if let, let else and while let will be part of the language at some point but are not prioritized atm.

Is it possible to match over a numbering group?

Amazing features, is there any example demonstrating the usage of pub?

numbering group = tuple of uints? not yet, but we’ll consider prioritizing further expansions to match based on community feedback

Hey, you can TAL at various place in the corelib, e.g. math.cairo, which no longer exposes everything by default

Amazing features and I loved to see storage of ByteArray :rocket:
NFTs we are coming :fire:

Excited about Cairo v2.5.0 - thanks for updating @FeedTheFed
While I’m more of an enthusiast than a developer, I’m always thrilled to see how each update brings us closer to realizing blockchain’s full potential. Keep up the great work, everyone!

Cheers!