Cairo 2.12.0 is out!

Cairo 2.12.0 was just released. This version only involves high-level compiler (Cairo→Sierra) changes and thus can be used to deploy contracts on Testnet and Mainnet without delay.

This version focuses on compilation time optimizations, affecting several facets of the development process:

  1. Improved diagnostics calculation time - this implies faster responses from the language server during development
  2. Compilation performance is substantially improved in this version, often reducing build times by dozens of percent and in some cases reaching more than 2x
  3. Incremental compilation - Scarb now includes a framework for caching, which implies that subsequent compilations will be faster

In addition, v2.12.0 includes the full integration of procedural macros in the language server, solving the longstanding issue in v2.11.x of snforge #[test] attributes being unknown. To fix this issue, make sure to:

  1. Use the new compiler version, v2.12.0 onwards, and have Cairo1 Proc Macros enabled in the extension (assuming you’re using VSCode):

  1. Use the upcoming version of snforge_std, v0.48.0 onwards (it will be published shortly after the 2.12.0 release). Enabling proc macros in the extension and using older versions of snforge_std will cause compilation errors in your tests to not be displayed correctly, marking the entire test rather than just the problematic line.
💡 For those optimizing dev workflows:

To maximize compile-time improvements during development, 
consider adding `inlining-strategy= "avoid"` to your development profile in your `Scarb.toml`. 
This reduces compilation time further, though it may increase test runtime and 
gas usage, potentially causing failures for tests with strict gas constraints.

We proceed to discuss a few of the noticeable updates of Cairo v2.12.0 (for an exhaustive list of changes, see the release notes):

let Chains

  • Following Rust v1.88, we introduce let chains that allow you to combine multiple conditions involving if let and while let, avoiding nested blocks:
if let Some(x) = get_x() && x > 0 && let Some(y) = get_y() && y > 0 {
    ...
}

Note that else is not supported yet for let chain expressions, this will be added in later versions.

let else

Cairo now supports let-else expressions, which allow refutable pattern matching, potentially diverging if the pattern does not match. This is illustrated in the following example:

enum MyEnum {
    A: u32,
    B: u32,
}

fn foo(a: MyEnum) {
    let MyEnum::A(x) = a else {
        bar(0);
        return;
    };
    bar(x);
}

fn bar(x: u32) { ... }

Declerative inline macros

So far, a developer has only been able to extend the compiler via procedural macros whose backend is written in Rust and is compiled alongside the Cairo project. In this version, we introduce declarative inline macros. The macro syntax follows the spirit of Rust’s v2 macros, emphasizing explicitness while still allowing some of the flexibility given by the (lack of) hygiene of macro_rules.

Declerative inline macros are still considered experimental and can be used by adding the following to your Scarb.toml:

experimental-features = ["user_defined_inline_macros"]

The following is an example of a simple add_one inline macro:

mod macros {
  pub macro add_one {
      ($x: ident) => { $x + 1 };
  }
}

mod test_macros {
  use macros::add_one;
  fn foo() {
    assert!(add_one!(2) == 3);
  }
}

Macros expect either identifiers or expressions, denoted by ident or expr similarly to Rust’s macro_rules!. We can slightly change our add_one macro to work on expressions.

mod macros {
  pub macro add_one {
      ($x: expr) => { $x + 1 };
  }
}

mod test_macros {
  use macros::add_one;
  fn foo() {
    let x = 5;
    assert!(add_one!(x+1) == 7);
  }
}

We can use items defined next to the macro definition or next to the application by using the $defsite and $callsite prefixes. This is illustrated in the following example:

mod macros {
  fn amount() -> u8 {
    10
  }
  pub macro add_amount_defsite {
      ($x: expr) => { $x + $defsite::amount() };
  }
  pub macro add_amount_callsite {
      ($x: expr) => { $x + $callsite::amount() };
  }

}

mod test_macros {
  use macros::add_amount_defsite;
  use macros::add_amount_callsite;

  fn amount() {
    20
  }

  fn foo() {
    let x = 5;
    assert!(add_one_defsite!(x) == 15);
    assert!(add_one_callsite!(x) == 25);
  }
}

Repetition macros are also defined similarly to Rust’s macro_rules!:

mod macros {
    pub macro add_some {
        ($($x:expr), *) => {
            {
                let mut sum = 0;
                $(sum += $x;)*
                sum
            }
        };
    }
}

mod test_macros {
  use macros::add_some;
  fn foo() {
    assert!(add_some!(1,2,3,4) == 10);
  }
}

Note that, similarly to Rust, macros are expected to expand to a single expression; thus, if your macro defines several statements, you should wrap them with an additional {} block.

If not specified, no data from the macro definition will leak to the macro callsite. This coincides with Rust’s planned v2 macro hygiene. For special cases that do require exposing data, we use the expose! macro to make new items available in the callsite:

pub macro shadow_variables {
    ($x:ident) => {
        expose!(let y = $x + 1;);
    };
}

mod test_macros {
  use macros::shadow_variables;
  fn foo() {
    let x = 5;
    let y = 5;
    shadow_variables!(x);
    assert!(y == 6);
  }
}

Note that exposed variables will be accessible to other inline macros called inside your macro definition. These can be accessed regularly via $callsite::var_name.

At the moment, macros that expand into items (structs, enums, functions, etc.) are not yet supported. Support for item macros will be added in future versions.

You can find more examples of macro definitions and usage in macro_test.cairo

Oracles

💡 This is an experimental feature.  
The API and behaviour may change in future versions of Scarb. 
Oracles are currently available in `scarb execute` (i.e., only available for 
Cairo executables used in a client-side proving context and not in Starknet contracts) 
with the `--experimental-oracles` flag.

Building upon the cairo-hints project by Reilabs, which introduced the ability for Cairo programs to receive external hints (via protobuf requests rather than inline Python as was done in Cairo Zero), Scarb 2.12.0 introduces the concept of oracles.

Rather than using external tools, Scarb now supports extensions to the Cairo runner that allow running arbitrary code and injecting the result back into the runner. This can be useful when you want to load inputs from the filesystem, or when you want to offload computation outside Cairo and only verify its result.

You can find an example of an Oracle usage here.

pub fn funny_hash(x: u64) -> oracle::Result<u64> {
    oracle::invoke(
        "stdio:cargo -q run --manifest-path my_oracle/Cargo.toml", 'funny_hash', (x),
    )
}

where oracle is defined in the Oracle package. The input to the invoke function is a connection string, selector, and a tuple containing the desired inputs. The connection string defines the communication between the CairoVM and the external computation. At the moment, only the stdio oracle protocol is supported, which works by spawning a subprocess that communicates with JSON-RPC over standard input/output.

The cairo_oracle_server crate helps define the Rust side of the oracle without worrying about the low-level details of stdio. In this example, you’ll find the backend that defines funny_hash. A developer using oracles only needs to initialize an oracle via Oracle::new() and add endpoints via provide, as shown below:

use anyhow::{Result, ensure};
use cairo_oracle_server::Oracle;
use starknet_core::codec::Encode;
use std::process::ExitCode;

#[derive(Encode)]
struct NumberAnalysis {
    both_are_odd: bool,
    mul: u128,
}

fn main() -> ExitCode {
    let mut accumulator = 0;

    Oracle::new()
        .provide("funny_hash", |value: u64| {
            ensure!(value % 2 == 0, "value must be even");
            Ok(value * value)
        })
        .provide(
            "zip_mul",
            |a: Vec<u64>, b: Vec<u64>| -> Result<Vec<NumberAnalysis>> {
                ensure!(a.len() == b.len(), "vectors must have the same length");
                Ok(a.into_iter()
                    .zip(b)
                    .map(|(x, y)| NumberAnalysis {
                        both_are_odd: x % 2 == 1 && y % 2 == 1,
                        mul: (x as u128) * (y as u128),
                    })
                    .collect())
            },
        )
        .provide("state_action", move |action: u64| {
            accumulator += action;
            Ok(accumulator)
        })
        .run()
}

The requirements for the closure argument of provide is for the inputs to implement the Decode trait and outputs to implement the Encode trait from starknet-rs. Note that Decode and Encode are already implemented in starknet-rs for most of Rust’s basic types, and for custom types, you can derive those traits as done in the example for the type NumberAnalysis, which is returned by the zip_mul handler.

Support for item macros will be added in future versions.

:smiling_face_with_tear:

Don’t be too sad, should be available on nightly soon

what are some practical use case of the oracle since it is only useful for client side applications?

it seems at the point where you’d need an oracle, you might as well use rust, or am I missing something?

It’s useful for reducing proving complexity. The classical example is proving that a is the square root of b. Instead of computing the root yourself in Cairo, you get a from an oracle and verify that a^2=b. Since squaring is much simpler than finding the root, you saved proving complexity (although the runner did exactly the same work, with the root finding code being in Rust rather than Cairo).