Cairo v2.5.0
match
expression Improvementswhile
loops- Storing strings
- The
pub
keyword - Experimental features - negative impls
- Compiler warnings
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 withpub
in package A will be visible whenever its imported from A (with an appropriateuse
statement)pub(crate)
- a definition marked withpub(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).