TL;DR
Cairo v2.9.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.
Continuing the efforts of v2.8.0, this version includes significant improvements to the language server, which should significantly improve the DevX. For those interested in more details, this is thanks to the transition from tower-lsp to lsp-server (the crate used by rust-analyzer), which is a major step in what is known in inner circles as the grand-rewrite.
Feature-wise, this version introduces basic Closures to Cairo. This will serve as the basis for popular Rust std-lib functions that we’ll add to the corelib in future versions, such as map
, map_err
, unwrap_or_else
etc. Also new are global uses, associated type bounds, syntax for renaming storage variables, and more.
Additionally, this version includes a new notion of a Cairo executable, which will serve as the basis for proving Cairo programs with Stwo.
We proceed to discuss a few of the noticeable updates of Cairo v2.9.0 (for an exhaustive list of changes, see the release notes):
- Breaking changes - no more transitive dependencies
- Closures
- Renaming storage variables
- Associated type specification
- Global uses
- Pow in the corelib
- Hex formatting support
- Locally generating proofs
Note: due to a bug related to the processing of procedural macros in the language server (which is still off by default until we’re convinced of its stability), Cairo v2.9.1 was released. To avoid issues, Scarb skipped to v2.9.1 directly (i.e. v2.9.0 will not be found in Scarb).
Breaking changes
With Scarb v2.9.1 onwards, transitive dependencies will no longer be accessible.
Similarly to Rust, if package A
has B
as a dependency, and C
has package A
as a dependency, then the code of C
will not be able to import things from B
without explicitly adding it to Scarb.toml
in addition to A
.
The motivation for the above is semver compatibility. If B has a breaking change, and A releases a minor version upgrading to B’s new version, then C may still break, although A did not, because it implicitly uses B directly. The fact that this was supported so far was more of a side-effect of the compilation model at the time rather than an intended state of affairs.
Closures
The FnOnce
and Fn
traits are added to the corelib. FnOnce
is implemented for closures that may consume captured variables, where Fn
is implemented for closures that capture only copyable variables.
The syntax for writing closures is that of Rust. For example, consider the following function defining a simple closure:
fn closure() {
let x = 8;
let c = |a| {
return x * (a + 3);
};
assert_eq!(c(2), 40);
}
Note: closures are still not allowed to capture mutable variables. This will be supported in future Cairo versions.
Below is a more complicated example for using closures, a potential version of Rust’s map_or in Cairo:
#[generate_trait]
impl MapOption of MapOptionTrait {
fn map_or<
T, F, + Drop<F>,
impl func: core::ops::FnOnce<F, (T,)>,
+Drop<func::Output>>(
self: Option<T>, default: func::Output, f: F) -> func::Output {
match self {
Option::None => default,
Option::Some(x) => f(x)
}
}
}
let a: Option<u32> = Option::Some(5);
assert!(a.map_or(42, |a: u32| a+a) == 10);
To make the above generic on the type of default
(as is done in Rust’s map_or
) we would need to use the experimental associated types specification which is explained in the following section:
#[generate_trait]
pub impl MapOption of MapOptionTrait {
fn map_or<
T, U, F, +Drop<U>, +Drop<F>,
impl func: core::ops::FnOnce<F, (T,)>[Output: U],
+Drop<func::Output>>(
self: Option<T>, default: U, f: F) -> func::Output {
match self {
Option::None => default,
Option::Some(x) => f(x)
}
}
}
For now, map_or
or other basic closure-based functionalities such as map
, unwrap_or
etc are not yet a part to the corelib and will be added in future Cairo versions.
Renaming storage variables
Many contracts must keep old storage variable names for backward compatibility. The original contract may be written in older Cairo versions, even Cairo0, but upgrading the contract had to rely on the original names even if they no longer “fit” the new code landscape. With the rename
attribute, you can now choose any name for your storage variables and keep the “legacy name” only in one place. This is illustrated in the following example:
#[storage]
pub struct Storage {
#[rename("legacy_name")]
new_name: Map<u8, u8>
}
self.new_name.write(1,1);
Although we must keep “legace_name” to correctly compute addresses of existing storage, we can write it once in the rename
attribute and use new_name
throughout the codebase.
Associated type specification
This is an experimental feature. To use it, add the following to your Scarb.toml
under the [package]
section:
experimental-features = ["associated_item_constraints"]
This new syntax allows us to specify what we want the associated types of generic impl params to be. We’ll illustrate this concept with an example involving iterators. Recall the iterator trait from the corelib:
pub trait Iterator<T> {
/// The type of the elements being iterated over.
type Item;
/// Advance the iterator and return the next value.
fn next(ref self: T) -> Option<Self::Item>;
}
How can we write a function with a generic parameters are T
, and an iterator I
whose item type is T
? Generic impl params allow us to have “trait bounds” on I::item
, but it doesn’t allow us to express equality. With the new syntax, we can write such a function as follows:
fn foo<T, I,
impl TIter: Iterator<I>[Item: T],
+Drop<I>, +Drop<T>, +Drop<TIter::Item>
>(collection: I) {
for elem in collection {
let a: T = elem;
}
}
At the moment, we still need to specify bounds on both T
and TIter::Item.
In the future, this will improve and be detected by our inference mechanism, allowing the above to be re-written as:
fn foo<T, I,
+Iterator<I>[Item: T], +Drop<I>, +Drop<T>
>(collection: I) {
for elem in collection {
let a: T = elem;
}
}
Global uses
The *
character may be used as the last segment of a use
path to import all importable entities from the entity of the preceding segment.
Similarly to Rust, we can shadow names from the global imports. This is illustrated by the following corelib test:
pub mod a {
pub const NON_SHADOWED: felt252 = 'non_shadowed';
pub const SHADOWED: felt252 = 'original_shadowed';
}
pub const SHADOWED: felt252 = 'new_shadowed';
use a::*;
#[test]
fn test_use_star_shadowing() {
assert_eq!(NON_SHADOWED, 'non_shadowed');
assert_eq!(SHADOWED, 'new_shadowed');
}
Pow in the corelib
Better late than never, the pow
trait and impl are added to the corelib for various integer types. Note that the exponent must be of type usize
.
assert_eq!((-2_i8).pow(0), 1);
assert_eq!((-2_i8).pow(1), -2);
assert_eq!((-2_i8).pow(2), 4);
assert_eq!((5_u32).pow(3), 125);
Hex formatting support
You can now use the "{:x}"
formatting notation:
assert!(format!("{:x}", 42) == "2a");
println!("{}", 42); // prints 42
println!("{:x}", 42); // prints 2a
Locally generating proofs
Integration with Stwo is still in the early stages; the relevant interfaces are not yet stable, and the code presented in this section may break in later Cairo versions.
We are starting to prepare for the release of the new prover! The stwo-cairo crate already contains basic functionality for proving the execution of Cairo programs. To prepare for the integration, we define a new executable struct, which is analogous to contract class for standalone programs.
The compiler can now generate this artifact from high-level Cairo, execute it, and prepare the execution trace expected by Stwo. We are now in the process of optimizing the DevX around proving in order to make the Stwo-based proving flow as simple as possible. The next big step is integration with Scarb, making proving as easy as running scarb build
and scarb prove
.
To mark a function as executable (and later provable), we introduced the #[executable]
attribute:
#[executable]
fn fib(a: u128, b: u128, n: u128) -> u128 {
if n == 0 {
a
} else {
fib(b, a + b, n - 1)
}
}
When compiling and generating the new executable, the resulting assembly adds the return values to the output segment. That is, inputs are private by default (not directly revealed by the proof), while outputs are public. For now, to publish part of the input, one needs to make sure to return it. In the future, we may add a dedicated syntax for marking inputs as public.