A compiler to convert Cairo's intermediate representation "Sierra" code
to machine code via MLIR and LLVM.
To get started on how to setup and run cairo-native check the getting started section.
To read more in-depth documentation, visit this page.
- Implemented Library Functions
- Getting started
- Command Line Interface
- Benchmarking
- API usage example
- From MLIR to native binary
🚧 cairo-native
is still being built therefore API breaking changes might happen often so use it at your own risk. 🚧
For versions under 1.0
cargo
doesn't comply with semver, so we advise to pin the version the version you use. This can be done by adding cairo-native = "0.1.0"
to your Cargo.toml
Cairo Native works by leveraging the intermediate representation of Cairo called Sierra. Sierra uses a list of builtin functions that implement the language functionality, those are called library functions, short: libfuncs. Basically every statement in a sierra program is a call to a libfunc, thus they are the core of Cairo Native progress towards feature parity.
This is a list of the current progress implementing each libfunc.
Implemented libfuncs (click to open)
alloc_local
array_append
array_get
array_len
array_new
array_pop_front_consume
array_pop_front
array_slice
array_snapshot_pop_back
array_snapshot_pop_front
bitwise
bool_and_impl
bool_not_impl
bool_or_impl
bool_to_felt252
bool_xor_impl
box_forward_snapshot
branch_align
bytes31_const
bytes31_to_felt252
bytes31_try_from_felt252
call_contract_syscall
(StarkNet)class_hash_to_felt252
(StarkNet)class_hash_try_from_felt252
(StarkNet)contract_address_const
(StarkNet)contract_address_to_felt252
(StarkNet)contract_address_try_from_felt252
(StarkNet)deploy_syscall
(StarkNet)disable_ap_tracking
downcast
drop
(3)dup
(3)ec_neg
ec_point_from_x_nz
ec_point_is_zero
ec_point_try_new_nz
ec_point_unwrap
ec_point_zero
ec_state_add_mul
ec_state_add
ec_state_init
ec_state_try_finalize_nz
emit_event_syscall
(StarkNet)enable_ap_tracking
enum_from_bounded_int
enum_init
enum_match
enum_snapshot_match
felt252_add_const
(4)felt252_add
felt252_const
felt252_dict_entry_finalize
felt252_dict_entry_get
felt252_dict_new
felt252_dict_squash
felt252_div_const
(4)felt252_div
(4)felt252_is_zero
felt252_mul_const
(4)felt252_mul
felt252_sub_const
(4)felt252_sub
finalize_locals
function_call
get_available_gas
get_block_hash_syscall
(StarkNet)get_builtin_costs
(5)get_execution_info_syscall
(StarkNet)hades_permutation
i128_diff
i16_diff
i32_diff
i64_diff
i8_diff
into_box
(2)jump
keccak_syscall
(StarkNet)library_call_syscall
(StarkNet)match_nullable
null
nullable_forward_snapshot
nullable_from_box
pedersen
print
rename
replace_class_syscall
(StarkNet)revoke_ap_tracking
send_message_to_l1_syscall
(StarkNet)snapshot_take
(1)span_from_tuple
storage_address_from_base_and_offset
(StarkNet)storage_address_from_base
(StarkNet)storage_address_to_felt252
(StarkNet)storage_address_try_from_felt252
(StarkNet)storage_base_address_const
(StarkNet)storage_base_address_from_felt252
(StarkNet)storage_read_syscall
(StarkNet)storage_write_syscall
(StarkNet)store_local
store_temp
struct_construct
struct_deconstruct
struct_snapshot_deconstruct
u128_byte_reverse
u128_const
u128_eq
u128_guarantee_mul
u128_is_zero
u128_mul_guarantee_verify
u128_overflowing_add
u128_overflowing_sub
u128_safe_divmod
u128_sqrt
u128_to_felt252
u128s_from_felt252
u16_const
u16_eq
u16_is_zero
u16_overflowing_add
u16_overflowing_sub
u16_safe_divmod
u16_sqrt
u16_to_felt252
u16_try_from_felt252
u16_wide_mul
u256_is_zero
u256_safe_divmod
u256_sqrt
u32_const
u32_eq
u32_is_zero
u32_overflowing_add
u32_overflowing_sub
u32_safe_divmod
u32_sqrt
u32_to_felt252
u32_try_from_felt252
u32_wide_mul
u64_const
u64_eq
u64_is_zero
u64_overflowing_add
u64_overflowing_sub
u64_safe_divmod
u64_sqrt
u64_to_felt252
u64_try_from_felt252
u64_wide_mul
u8_const
u8_eq
u8_is_zero
u8_overflowing_add
u8_overflowing_sub
u8_safe_divmod
u8_sqrt
u8_to_felt252
u8_try_from_felt252
u8_wide_mul
unbox
(2)unwrap_non_zero
upcast
withdraw_gas_all
(5)withdraw_gas
(5)
Not yet implemented libfuncs (click to open)
const_as_box
secp256k1_add_syscall
(StarkNet)secp256k1_get_point_from_x_syscall
(StarkNet)secp256k1_get_xy_syscall
(StarkNet)secp256k1_mul_syscall
(StarkNet)secp256k1_new_syscall
(StarkNet)secp256r1_add_syscall
(StarkNet)secp256r1_get_point_from_x_syscall
(StarkNet)secp256r1_get_xy_syscall
(StarkNet)secp256r1_mul_syscall
(StarkNet)secp256r1_new_syscall
(StarkNet)
Not yet implemented libfuncs (testing category only, click to open)
Testing libfuncs:pop_log
(StarkNet, testing)redeposit_gas
set_account_contract_address
(StarkNet, testing)set_block_number
(StarkNet, testing)set_block_timestamp
(StarkNet, testing)set_caller_address
(StarkNet, testing)set_chain_id
(StarkNet, testing)set_contract_address
(StarkNet, testing)set_max_fee
(StarkNet, testing)set_nonce
(StarkNet, testing)set_sequencer_address
(StarkNet, testing)set_signature
(StarkNet, testing)set_transaction_hash
(StarkNet, testing)set_version
(StarkNet, testing)
Footnotes on the libfuncs list:
- It is implemented but we're not handling potential issues like lifetimes yet.
- It is implemented but we're still debating whether it should be a Rust-like
Box<T>
or if it's fine treating it like another variable. - It is implemented but side-effects are not yet handled (ex. array cloning/dropping).
- Not supported by the Cairo to Sierra compiler.
- Implemented with a dummy. It doesn't do anything yet.
- Linux or macOS (aarch64 included) only for now
- LLVM 17 with MLIR: On debian you can use apt.llvm.org, on macOS you can use brew
- Nightly Rust
- Git
This step applies to all operating systems.
Run the following make target to install the dependencies (both Linux and macOS):
make deps
Since Linux distributions change widely, you need to install LLVM 17 via your package manager, compile it or check if the current release has a Linux binary.
If you are on Debian/Ubuntu, check out the repository https://apt.llvm.org/ Then you can install with:
sudo apt-get install llvm-17 llvm-17-dev llvm-17-runtime clang-17 clang-tools-17 lld-17 libpolly-17-dev libmlir-17-dev mlir-17-tools
If you decide to build from source, here are some indications:
Install LLVM from source instructions
# Go to https://github.com/llvm/llvm-project/releases
# Download the latest LLVM 17 release:
# The blob to download is called llvm-project-17.x.x.src.tar.xz
# For example
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-17.0.3/llvm-project-17.0.3.src.tar.xz
tar xf llvm-project-17.0.3.src.tar.xz
cd llvm-project-17.0.3.src.tar
mkdir build
cd build
# The following cmake command configures the build to be installed to /opt/llvm-17
cmake -G Ninja ../llvm \
-DLLVM_ENABLE_PROJECTS="mlir;clang;clang-tools-extra;lld;polly" \
-DLLVM_BUILD_EXAMPLES=OFF \
-DLLVM_TARGETS_TO_BUILD="Native" \
-DCMAKE_INSTALL_PREFIX=/opt/llvm-17 \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DLLVM_PARALLEL_LINK_JOBS=4 \
-DLLVM_ENABLE_BINDINGS=OFF \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_ENABLE_LLD=ON \
-DLLVM_ENABLE_ASSERTIONS=OFF
ninja install
Setup a environment variable called MLIR_SYS_170_PREFIX
, LLVM_SYS_170_PREFIX
and TABLEGEN_170_PREFIX
pointing to the llvm directory:
# For Debian/Ubuntu using the repository, the path will be /usr/lib/llvm-17
export MLIR_SYS_170_PREFIX=/usr/lib/llvm-17
export LLVM_SYS_170_PREFIX=/usr/lib/llvm-17
export TABLEGEN_170_PREFIX=/usr/lib/llvm-17
Run the deps target to install the other dependencies such as the cairo compiler (for tests, benchmarks).
make deps
The makefile deps
target (which you should have ran before) installs LLVM 17 with brew for you, afterwards you need to execute the env-macos.sh
script to setup the
needed environment variables.
source env-macos.sh
Running make
by itself will list available targets.
- Install the necessary dependencies (on Linux, you need to get LLVM 17 manually):
make deps
- Build a release version:
make build
Or with your native CPU Architecture for even more performance (usually):
make build-native
- Install the
cairo-native-dump
andcairo-native-run
commands:
make install
- Build a optimized development version:
make build-dev
- View and open the docs:
make doc-open
- Run the tests:
make test
- Generate coverage:
make coverage
- Run clippy and format checks:
make check
- Compile the runtime library used for ahead of time compilation:
make runtime
cairo-native-dump
:
Usage: cairo-native-dump [OPTIONS] <INPUT>
Arguments:
<INPUT>
Options:
-o, --output <OUTPUT> [default: -]
-h, --help Print help
cairo-native-run
:
This tool allows to run programs using the JIT engine, like the cairo-run
tool, the parameters can only be felt values.
echo '1' | cairo-native-run 'program.cairo' 'program::program::main' --inputs - --outputs -
Usage: cairo-native-run [OPTIONS] <INPUT> <ENTRY_POINT>
Arguments:
<INPUT>
<ENTRY_POINT>
Options:
-i, --inputs <INPUTS>
-o, --outputs <OUTPUTS>
-p, --print-outputs
-h, --help Print help
This is a usage example using the API for an easy Cairo program that requires the least setup to get running. It allows you to compile and execute a program using the JIT.
Example code to run a program:
use starknet_types_core::felt::Felt;
use cairo_native::context::NativeContext;
use cairo_native::executor::NativeExecutor;
use cairo_native::values::JitValue;
use std::path::Path;
fn main() {
let program_path = Path::new("programs/examples/hello.cairo");
// Compile the cairo program to sierra.
let sierra_program = cairo_native::utils::cairo_to_sierra(program_path);
// Instantiate a Cairo Native MLIR context. This data structure is responsible for the MLIR
// initialization and compilation of sierra programs into a MLIR module.
let native_context = NativeContext::new();
// Compile the sierra program into a MLIR module.
let native_program = native_context.compile(&sierra_program).unwrap();
// The parameters of the entry point.
let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))];
// Find the entry point id by its name.
let entry_point = "hello::hello::greet";
let entry_point_id = cairo_native::utils::find_function_id(&sierra_program, entry_point);
// Instantiate the executor.
let native_executor = NativeExecutor::new(native_program);
// Execute the program.
let result = native_executor
.execute(entry_point_id, params, None)
.unwrap();
println!("Cairo program was compiled and executed successfully.");
println!("{:?}", result);
}
Example code to run a Starknet contract:
use starknet_types_core::felt::Felt;
use cairo_lang_compiler::CompilerConfig;
use cairo_lang_starknet::contract_class::compile_path;
use cairo_native::context::NativeContext;
use cairo_native::executor::NativeExecutor;
use cairo_native::utils::find_entry_point_by_idx;
use cairo_native::values::JitValue;
use cairo_native::{
metadata::syscall_handler::SyscallHandlerMeta,
starknet::{BlockInfo, ExecutionInfo, StarkNetSyscallHandler, SyscallResult, TxInfo, U256},
};
use std::path::Path;
/// To run a starknet contract, we need to use a syscall handler, here we show how to implement one (at the end).
#[derive(Debug)]
struct SyscallHandler;
fn main() {
let path = Path::new("programs/examples/hello_starknet.cairo");
let contract = compile_path(
path,
None,
CompilerConfig {
replace_ids: true,
..Default::default()
},
)
.unwrap();
let entry_point = contract.entry_points_by_type.constructor.get(0).unwrap();
let sierra_program = contract.extract_sierra_program().unwrap();
let native_context = NativeContext::new();
let mut native_program = native_context.compile(&sierra_program).unwrap();
native_program
.insert_metadata(SyscallHandlerMeta::new(&mut SyscallHandler))
.unwrap();
// Call the echo function from the contract using the generated wrapper.
let entry_point_fn =
find_entry_point_by_idx(&sierra_program, entry_point.function_idx).unwrap();
let fn_id = &entry_point_fn.id;
let native_executor = NativeExecutor::new(native_program);
let result = native_executor
.execute_contract(
fn_id,
// The calldata
&[JitValue::Felt252(Felt::from(1))],
u64::MAX.into(),
)
.expect("failed to execute the given contract");
println!();
println!("Cairo program was compiled and executed successfully.");
println!("{result:#?}");
}
// Implement an example syscall handler.
impl StarkNetSyscallHandler for SyscallHandler {
fn get_block_hash(
&mut self,
block_number: u64,
_gas: &mut u128,
) -> SyscallResult<Felt> {
println!("Called `get_block_hash({block_number})` from MLIR.");
Ok(Felt::from_bytes_be_slice(b"get_block_hash ok"))
}
fn get_execution_info(
&mut self,
_gas: &mut u128,
) -> SyscallResult<cairo_native::starknet::ExecutionInfo> {
println!("Called `get_execution_info()` from MLIR.");
Ok(ExecutionInfo {
block_info: BlockInfo {
block_number: 1234,
block_timestamp: 2345,
sequencer_address: 3456.into(),
},
tx_info: TxInfo {
version: 4567.into(),
account_contract_address: 5678.into(),
max_fee: 6789,
signature: vec![1248.into(), 2486.into()],
transaction_hash: 9876.into(),
chain_id: 8765.into(),
nonce: 7654.into(),
},
caller_address: 6543.into(),
contract_address: 5432.into(),
entry_point_selector: 4321.into(),
})
}
fn deploy(
&mut self,
class_hash: Felt,
contract_address_salt: Felt,
calldata: &[Felt],
deploy_from_zero: bool,
_gas: &mut u128,
) -> SyscallResult<(Felt, Vec<Felt>)> {
println!("Called `deploy({class_hash}, {contract_address_salt}, {calldata:?}, {deploy_from_zero})` from MLIR.");
Ok((
class_hash + contract_address_salt,
calldata.iter().map(|x| x + &Felt::from(1)).collect(),
))
}
fn replace_class(
&mut self,
class_hash: Felt,
_gas: &mut u128,
) -> SyscallResult<()> {
println!("Called `replace_class({class_hash})` from MLIR.");
Ok(())
}
fn library_call(
&mut self,
class_hash: Felt,
function_selector: Felt,
calldata: &[Felt],
_gas: &mut u128,
) -> SyscallResult<Vec<Felt>> {
println!(
"Called `library_call({class_hash}, {function_selector}, {calldata:?})` from MLIR."
);
Ok(calldata.iter().map(|x| x * Felt::from(3)).collect())
}
fn call_contract(
&mut self,
address: Felt,
entry_point_selector: Felt,
calldata: &[Felt],
_gas: &mut u128,
) -> SyscallResult<Vec<Felt>> {
println!(
"Called `call_contract({address}, {entry_point_selector}, {calldata:?})` from MLIR."
);
Ok(calldata.iter().map(|x| x * Felt::from(3)).collect())
}
fn storage_read(
&mut self,
address_domain: u32,
address: Felt,
_gas: &mut u128,
) -> SyscallResult<Felt> {
println!("Called `storage_read({address_domain}, {address})` from MLIR.");
Ok(address * Felt::from(3))
}
fn storage_write(
&mut self,
address_domain: u32,
address: Felt,
value: Felt,
_gas: &mut u128,
) -> SyscallResult<()> {
println!("Called `storage_write({address_domain}, {address}, {value})` from MLIR.");
Ok(())
}
fn emit_event(
&mut self,
keys: &[Felt],
data: &[Felt],
_gas: &mut u128,
) -> SyscallResult<()> {
println!("Called `emit_event({keys:?}, {data:?})` from MLIR.");
Ok(())
}
fn send_message_to_l1(
&mut self,
to_address: Felt,
payload: &[Felt],
_gas: &mut u128,
) -> SyscallResult<()> {
println!("Called `send_message_to_l1({to_address}, {payload:?})` from MLIR.");
Ok(())
}
fn keccak(
&mut self,
input: &[u64],
_gas: &mut u128,
) -> SyscallResult<cairo_native::starknet::U256> {
println!("Called `keccak({input:?})` from MLIR.");
Ok(U256(Felt::from(1234567890).to_le_bytes()))
}
/*
... more code here, check out the full example in examples/starknet.rsd
*/
}
For more examples, check out the examples/
directory.
- hyperfine:
cargo install hyperfine
- cairo >=1.0
- Cairo Corelibs
- LLVM 16 with MLIR
You need to setup some environment variables:
$MLIR_SYS_170_PREFIX=/path/to/llvm17 # Required for non-standard LLVM install locations.
$LLVM_SYS_170_PREFIX=/path/to/llvm17 # Required for non-standard LLVM install locations.
$TABLEGEN_170_PREFIX=/path/to/llvm17 # Required for non-standard LLVM install locations.
make bench
The bench
target will run the ./scripts/bench-hyperfine.sh
script.
This script runs hyperfine commands to compare the execution time of programs in the ./programs/benches/
folder.
Each program is compiled and executed via the execution engine with the cairo-native-run
command and via the cairo-vm with the cairo-run
command provided by the cairo
codebase.
The cairo-run
command should be available in the $PATH
and ideally compiled with cargo build --release
.
If you want the benchmarks to run using a specific build, or the cairo-run
commands conflicts with something (e.g. the cairo-svg package binaries in macos) then the command to run cairo-run
with a full path can be specified with the $CAIRO_RUN
environment variable.
# to mlir with llvm dialect
sierra2mlir program.sierra -o program.mlir
# translate all dialects to the llvm dialect
"$MLIR_SYS_170_PREFIX/bin/mlir-opt" \
--canonicalize \
--convert-scf-to-cf \
--canonicalize \
--cse \
--expand-strided-metadata \
--finalize-memref-to-llvm \
--convert-func-to-llvm \
--convert-index-to-llvm \
--reconcile-unrealized-casts \
"program.mlir" \
-o "program-llvm.mlir"
# translate mlir to llvm-ir
"$MLIR_SYS_170_PREFIX"/bin/mlir-translate --mlir-to-llvmir program-llvm.mlir -o program.ll
# compile natively
"$MLIR_SYS_170_PREFIX"/bin/clang program.ll -Wno-override-module \
-L "$MLIR_SYS_170_PREFIX"/lib -L"./target/release/" \
-lsierra2mlir_utils -lmlir_c_runner_utils \
-Wl,-rpath "$MLIR_SYS_170_PREFIX"/lib \
-Wl,-rpath ./target/release/ \
-o program
./program
This tool mimics the cairo-test
tool and is identical to it, the only feature it doesn't have is the profiler.
You can download it on our releases page.
$ cairo-native-test --help
Compiles a Cairo project and runs all the functions marked as `#[test]`.
Exits with 1 if the compilation or run fails, otherwise 0.
Usage: cairo-native-test [OPTIONS] <PATH>
Arguments:
<PATH> The Cairo project path to compile and run its tests
Options:
-s, --single-file Whether path is a single file
--allow-warnings Allows the compilation to succeed with warnings
-f, --filter <FILTER> The filter for the tests, running only tests containing the filter string [default: ]
--include-ignored Should we run ignored tests as well
--ignored Should we run only the ignored tests
--starknet Should we add the starknet plugin to run the tests
--run-mode <RUN_MODE> Run with JIT or AOT (compiled) [default: jit] [possible values: aot, jit]
-O, --opt-level <OPT_LEVEL> Optimization level, Valid: 0, 1, 2, 3. Values higher than 3 are considered as 3 [default: 0]
-h, --help Print help
-V, --version Print version
For single files, you can use the -s, --single-file
option.
For a project, it needs to have a cairo_project.toml
specifying the crate_roots. You can find an
example under the cairo-tests/
folder, which is a cairo project that works with this tool.
cairo-native-test -s myfile.cairo
cairo-native-test ./cairo-tests/
This will run all the tests (functions marked with the #[test]
attribute).
These 2 env vars will dump the generated MLIR code from any compilation on the current working directory as:
dump.mlir
: The MLIR code after passes without locations.dump-debug.mlir
: The MLIR code after passes with locations.dump-prepass.mlir
: The MLIR code before without locations.dump-prepass-debug.mlir
: The MLIR code before passes with locations.
Do note that the MLIR with locations is in pretty form and thus not suitable to pass to mlir-opt
.
export NATIVE_DEBUG_DUMP_PREPASS=1
export NATIVE_DEBUG_DUMP=1
Enable logging to see the compilation process:
export RUST_LOG="cairo_native=trace"
Other tips:
- Try to find the minimal program to reproduce an issue, the more isolated the easier to test.
- Use the
debug_utils
print utilities, more info here:
#[cfg(feature = "with-debug-utils")]
{
metadata.get_mut::<DebugUtils>()
.unwrap()
.print_pointer(context, helper, entry, ptr, location)?;
}