Cairo 2.11.0 was just released. This version only involves high-level compiler (Cairo→Sierra) changes and thus can be used to deploy contracts on Starknet v0.13.4. This means that Cairo 2.11.0 is usable on Testnet without delay and will be usable on Mainnet later this month.
We proceed to discuss a few of the noticeable updates of Cairo v2.10.0 (for an exhaustive list of changes, see the release notes):
- Corelib updates
- Using enum variants
- Consts updates
- Storage scrubbing
- New
Vec
interface - Early return and error propagation in loops
- No more ; after loops
- Deref is extended to methods
In addition to changes in Cairo, the language server continues to improve and create a smoother dev experience, you can read about the recent and upcoming changes in this tweet thread.
Another notable change is Scarb’s support for procedural macros. While already in v2.11.0, the compiler<>macro interface is expected to change in the next version. Given the scope of the feature, procedural macros will get their own post in the coming weeks alongside the new interface.
Breaking changes
While not a breaking change, we’d like to use this opportunity to remind developers that Cairo ≥ 2.10.0 requires starknet-foundry ≥ 0.38.0 to function properly.
The new change introduced with Cairo 2.11.0 is that dependency on snforge_std
< 0.38.0 (note that we distinguish snforge_std the library, from foundry the testing engine, as one can use new foundry versions with older snforge_std
libraries), then for tests to compile you need to add the following to your dev dependencies section in your Scarb.toml:
snforge_std = "x.y.z"
snforge_scarb_plugin = "x.y.z"
That is, whenever you depend on snforge_std
, add a dependency on snforge_scarb_plugin
with the same version.
Note that this is NOT required when using snforge_std
≥ 0.38.0.
Corelib updates
Iterator trait
In Cairo v2.11.0 the iterator trait is much richer, and includes the following functions:
count
last
advance_by
nth
map
enumerate
fold
any
all
find
filter
zip
collect
peekable
take
sum
product
chain
You can find a few usage examples below, for more examples, see iter_test.cairo in the corelib:
let mut iter = array![1, 2, 3].into_iter().map(|x| 2 * x);
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(4));
assert_eq!(iter.next(), Some(6));
assert_eq!(iter.next(), None);
for (index, elem) in arr.into_iter().enumerate() {
...
}
let mut iter = array![0_u32, 1, 2].into_iter().filter(|x| *x > 0);
assert_eq!(iter.next(), Option::Some(1));
assert_eq!(iter.next(), Option::Some(2));
assert_eq!(iter.next(), Option::None);
let mut iter = array![1, 2, 3].into_iter();
let sum = iter.fold(0, |acc, x| acc + x);
assert_eq!(sum, 6);
Option trait
The following functions are added to OptionTrait:
ok_or_else
and
and_then
or
or_else
xor
is_some_and
is_none_or
unwrap_or_else
map
map_or
map_or_else
take
filter
flatten
You can find a few usage examples below, for more examples, see option_test.cairo in the corelib:
let maybe_some_string: Option<ByteArray> = Some("Hello, World!");
let maybe_some_len = maybe_some_string.map(|s| s.len());
assert!(maybe_some_len == Some(13));
let k = 21;
let mut x = Some("foo");
assert_eq!(x.map_or_else(|| 2 * k, |v: ByteArray| v.len()), 3);
x = None;
assert_eq!(x.map_or_else(|| 2 * k, |v: ByteArray| v.len()), 42);
let option: Option<felt252> = None;
assert_eq!(option.ok_or_else(|| 0), Err(0));
let x: Option<Option<u32>> = Some(Some(6));
assert_eq!(Some(6), x.flatten());
Using enum variants
You can now import concrete enum variants, and refer to them without the full path:
mod definitions {
enum MyEnum {
Var1,
Var2
}
}
use definitions::MyEnum::{Var1, Var2}
let my_enum = Var1;
match my_enum {
Var1 => 1,
Var2 => 2
}
In particular, the prelude now includes Option::Some
and Option::None
, so you can write the following without using the Option::
prefix:
let something = Some(3);
let nothing = None;
Consts updates
Const ContractAddress
and ClassHash
You can now instantiate constants of the ContractAddress
and ClassHash
types, the generic functions contract_address_const
and class_hash_const
are no longer needed:
use starknet::{ContractAddress, ClassHash};
const class_hash: ClassHash = 0x123.try_into().unwrap();
const contract_address: ContractAddress = 0x123.try_into().unwrap();
Note that we don’t have dedicated literals for those types, hence conversions are necessary.
Const functions
Functions that can be evaluated at compile time can now be marked as const via the const fn
syntax, similarly to Rust:
use core::num::traits::Pow;
const mask: u32 = 2_u32.pow(20)
Several functions in the corelib are now marked by const
, in fact this was used in the previous section where we applied try_into
and unwrap
in a const
expression.
Storage scrubbing
The Store
trait now the additional scrub function:
fn scrub(
address_domain: u32, base: StorageBaseAddress, offset: u8,
) -> SyscallResult<()>
Since this function has a defsiault implementation, this is a non-breaking change. With scrubbing, you can “remove” from storage by writing zeros instead of the existing value.
Note that scrubbing storage is an expensive operation: scrubbing will cost
32*TYPE_SIZE
L1 blob gas. For example, a struct with 2 u256
members will cost 32*4=128
blob gas to scrub.
New Vec
interface
The MutableVecTrait
now includes the following functions:
fn allocate(self: T) -> StoragePath<Mutable<Self::ElementType>>;
fn push<+Drop<Self::ElementType>, +starknet::Store<Self::ElementType>>(
self: T, value: Self::ElementType,
);
fn pop<+Drop<Self::ElementType>, +starknet::Store<Self::ElementType>>(
self: T,
) -> Option<Self::ElementType>;
Following community feedback, we’re adding the more intuitive push
and pop
interface to Vec
, which replaces the append
and write
flow. For backward compatibility purposes, the old functions still exist in the Vec
trait, but are marked as deprecated.
Below you can find an example of interaction with the new Vec
interface:
use starknet::storage::{
StoragePointerReadAccess,
StoragePointerWriteAccess,
Vec,
MutableVecTrait
};
#[storage]
struct Storage {
my_vec: Vec<u8>
}
...
self.my_vec.push(1);
self.my_vec.push(2);
self.my_vec.push(3);
assert_eq!(self.my_vec[0].read(), 1);
assert_eq!(self.my_vec[1].read(), 2);
assert_eq!(self.my_vec[2].read(), 3);
// pop
assert_eq!(self.my_vec.pop(), Some(3));
assert_eq!(self.my_vec.len(), 2);
Note that the pop function is costly as it involves a call to
scrub
, which involves multiple storage writes (depending on the underlying type’s size).
The append
function is now deprecated, and using it will emit a warning (which can be canceled by using the starknet-storage-deprecation
feature). When you have a Vec
of types that do not implement the Store
trait, e.g. a vector of storage nodes, then you’ll need to use the new allocate
function, which is similar to the deprecated append
(in fact, they have exactly the same implementation):
use starknet::storage::{
Vec,
MutableVecTrait,
Map,
StorageMapReadAccess,
StorageMapWriteAccess,
StoragePointerReadAccess,
StoragePointerWriteAccess
};
#[starknet::storage_node]
struct node {
map: Map<u8,u8>
}
#[storage]
struct Storage {
my_vec: Vec<node>
}
...
self.my_vec.allocate().map.write(1, 1);
To add a new node to my_vec
, we must use the allocate
function since node
itself cannot be instantiated (it only serves to indicate storage paths via its members) and hence can never be an argument for push
.
Early return and error propagation in loops
We can now have return statements and use the ?
operator inside loops:
fn foo() -> Result<u8, ByteArray> {
// error propagation
for i in 1..10_u64 {
let _converted: u8 = i.try_into().ok_or("fail to convert u64 into u8")?;
}
// early return
for i in 1..100_u64 {
if (i == 42) {
return Ok(42);
}
}
Ok(42)
}
No more ; after curly brackets
Semicolon is no longer required after a loop block, i.e. the following is out:
for i in 1..10_u8 {
println!("{}", i);
};
This is in:
for i in 1..10_u8 {
println!("{}", i)
}
Deref is extended to methods
In Cairo v2.7.0 we introduced the Deref
and DerefMut
trait, which allowed transparent access to the members of type Dest
from an instance of type Target
when there is an impl of Deref<Target, Dest>
in the current context. Now, this mechanism is extended to methods that operate on the Dest
type, as demonstrated by the code below:
use core::ops::Deref;
struct MySource {
pub data: u8
}
struct MyTarget {
pub data: u8
}
#[generate_trait]
impl TargetMutImpl of TargetTrait {
fn foo(ref self: MyTarget) -> u8 {
self.data
}
}
impl SourceDeref of Deref<MySource> {
type Target = MyTarget;
fn deref(self: MySource) -> MyTarget {
MyTarget {
data: self.data
}
}
}
Thanks to the Deref
impl, we can call foo
on MySource
:
let mut source = MySource { data: 5 };
let res = source.foo(5);