From a282628f56f2b74dd20ceab2c27c4b9049f95efd Mon Sep 17 00:00:00 2001 From: Esteve Soler Arderiu Date: Wed, 11 Dec 2024 17:54:21 -0300 Subject: [PATCH 01/35] Add some docs. Fix some issues. --- benches/compile_time.rs | 1 - benches/util.rs | 2 +- cairo-tests/src/integer_test.cairo | 1939 ---------------------------- docs/compilation_walkthrough.md | 308 +++-- src/compiler.rs | 27 +- src/metadata/gas.rs | 17 +- 6 files changed, 208 insertions(+), 2086 deletions(-) delete mode 100644 cairo-tests/src/integer_test.cairo diff --git a/benches/compile_time.rs b/benches/compile_time.rs index d6caa5a7cd..ec3b5a8e82 100644 --- a/benches/compile_time.rs +++ b/benches/compile_time.rs @@ -98,5 +98,4 @@ pub fn bench_compile_time(c: &mut Criterion) { } criterion_group!(benches, bench_compile_time); - criterion_main!(benches); diff --git a/benches/util.rs b/benches/util.rs index 813898c234..9fda9bf2d2 100644 --- a/benches/util.rs +++ b/benches/util.rs @@ -20,7 +20,7 @@ pub fn prepare_programs(path: &str) -> Vec<(Arc, String)> { .collect::>() } -#[allow(unused)] // its used but clippy doesn't detect it well +#[allow(unused)] // Used in `benches/libfuncs.rs`, but not in the others. pub fn create_vm_runner(program: &Program) -> SierraCasmRunner { SierraCasmRunner::new( program.clone(), diff --git a/cairo-tests/src/integer_test.cairo b/cairo-tests/src/integer_test.cairo deleted file mode 100644 index 14633854e0..0000000000 --- a/cairo-tests/src/integer_test.cairo +++ /dev/null @@ -1,1939 +0,0 @@ -use core::{ - integer, - integer::{ - BoundedInt, u128_sqrt, u128_wrapping_sub, u16_sqrt, u256_sqrt, u256_wide_mul, u32_sqrt, - u512_safe_div_rem_by_u256, u512, u64_sqrt, u8_sqrt - } -}; -use core::test::test_utils::{assert_eq, assert_ne, assert_le, assert_lt, assert_gt, assert_ge}; - -#[test] -fn test_u8_operators() { - assert_eq(@1_u8, @1_u8, '1 == 1'); - assert_ne(@1_u8, @2_u8, '1 != 2'); - assert_eq(@(1_u8 + 3_u8), @4_u8, '1 + 3 == 4'); - assert_eq(@(3_u8 + 6_u8), @9_u8, '3 + 6 == 9'); - assert_eq(@(3_u8 - 1_u8), @2_u8, '3 - 1 == 2'); - assert_eq(@(1_u8 * 3_u8), @3_u8, '1 * 3 == 3'); - assert_eq(@(2_u8 * 4_u8), @8_u8, '2 * 4 == 8'); - assert_eq(@(19_u8 / 7_u8), @2_u8, '19 / 7 == 2'); - assert_eq(@(19_u8 % 7_u8), @5_u8, '19 % 7 == 5'); - assert_eq(@(231_u8 - 131_u8), @100_u8, '231-131=100'); - assert_eq(@((1_u8 | 2_u8)), @3_u8, '1 | 2 == 3'); - assert_eq(@((1_u8 & 2_u8)), @0_u8, '1 & 2 == 0'); - assert_eq(@((1_u8 ^ 2_u8)), @3_u8, '1 ^ 2 == 3'); - assert_eq(@((2_u8 | 2_u8)), @2_u8, '2 | 2 == 2'); - assert_eq(@((2_u8 & 2_u8)), @2_u8, '2 & 2 == 2'); - assert_eq(@((2_u8 & 3_u8)), @2_u8, '2 & 3 == 2'); - assert_eq(@((3_u8 ^ 6_u8)), @5_u8, '3 ^ 6 == 5'); - assert_lt(1_u8, 4_u8, '1 < 4'); - assert_le(1_u8, 4_u8, '1 <= 4'); - assert(!(4_u8 < 4_u8), '!(4 < 4)'); - assert_le(5_u8, 5_u8, '5 <= 5'); - assert(!(5_u8 <= 4_u8), '!(5 <= 8)'); - assert_gt(5_u8, 2_u8, '5 > 2'); - assert_ge(5_u8, 2_u8, '5 >= 2'); - assert(!(3_u8 > 3_u8), '!(3 > 3)'); - assert_ge(3_u8, 3_u8, '3 >= 3'); - assert_eq(@u8_sqrt(9), @3, 'u8_sqrt(9) == 3'); - assert_eq(@u8_sqrt(10), @3, 'u8_sqrt(10) == 3'); - assert_eq(@u8_sqrt(0x40), @0x8, 'u8_sqrt(2^6) == 2^3'); - assert_eq(@u8_sqrt(0xff), @0xf, 'Wrong square root result.'); - assert_eq(@u8_sqrt(1), @1, 'u8_sqrt(1) == 1'); - assert_eq(@u8_sqrt(0), @0, 'u8_sqrt(0) == 0'); - assert_eq(@~0x00_u8, @0xff, '~0x00 == 0xff'); - assert_eq(@~0x81_u8, @0x7e, '~0x81 == 0x7e'); -} - -#[test] -#[should_panic] -fn test_u8_sub_overflow_1() { - 0_u8 - 1_u8; -} - -#[test] -#[should_panic] -fn test_u8_sub_overflow_2() { - 0_u8 - 3_u8; -} - -#[test] -#[should_panic] -fn test_u8_sub_overflow_3() { - 1_u8 - 3_u8; -} - -#[test] -#[should_panic] -fn test_u8_sub_overflow_4() { - 100_u8 - 250_u8; -} - -#[test] -#[should_panic] -fn test_u8_add_overflow_1() { - 128_u8 + 128_u8; -} - -#[test] -#[should_panic] -fn test_u8_add_overflow_2() { - 200_u8 + 60_u8; -} - -#[test] -#[should_panic] -fn test_u8_mul_overflow_1() { - 0x10_u8 * 0x10_u8; -} - -#[test] -#[should_panic] -fn test_u8_mul_overflow_2() { - 0x11_u8 * 0x10_u8; -} - -#[test] -#[should_panic] -fn test_u8_mul_overflow_3() { - 2_u8 * 0x80_u8; -} - -#[test] -#[should_panic(expected: ('Division by 0',))] -fn test_u8_div_by_0() { - 2_u8 / 0_u8; -} - -#[test] -#[should_panic] -fn test_u8_mod_by_0() { - 2_u8 % 0_u8; -} - -#[test] -fn test_u16_operators() { - assert_eq(@1_u16, @1_u16, '1 == 1'); - assert_ne(@1_u16, @2_u16, '1 != 2'); - assert_eq(@(1_u16 + 3_u16), @4_u16, '1 + 3 == 4'); - assert_eq(@(3_u16 + 6_u16), @9_u16, '3 + 6 == 9'); - assert_eq(@(3_u16 - 1_u16), @2_u16, '3 - 1 == 2'); - assert_eq(@(231_u16 - 131_u16), @100_u16, '231-131=100'); - assert_eq(@(1_u16 * 3_u16), @3_u16, '1 * 3 == 3'); - assert_eq(@(2_u16 * 4_u16), @8_u16, '2 * 4 == 8'); - assert_eq(@(51725_u16 / 7_u16), @7389_u16, '51725 / 7 == 7389'); - assert_eq(@(51725_u16 % 7_u16), @2_u16, '51725 % 7 == 2'); - assert_eq(@((1_u16 | 2_u16)), @3_u16, '1 | 2 == 3'); - assert_eq(@((1_u16 & 2_u16)), @0_u16, '1 & 2 == 0'); - assert_eq(@((1_u16 ^ 2_u16)), @3_u16, '1 ^ 2 == 3'); - assert_eq(@((2_u16 | 2_u16)), @2_u16, '2 | 2 == 2'); - assert_eq(@((2_u16 & 2_u16)), @2_u16, '2 & 2 == 2'); - assert_eq(@((2_u16 & 3_u16)), @2_u16, '2 & 3 == 2'); - assert_eq(@((3_u16 ^ 6_u16)), @5_u16, '3 ^ 6 == 5'); - assert_lt(1_u16, 4_u16, '1 < 4'); - assert_le(1_u16, 4_u16, '1 <= 4'); - assert(!(4_u16 < 4_u16), '!(4 < 4)'); - assert_le(4_u16, 4_u16, '4 <= 4'); - assert_gt(5_u16, 2_u16, '5 > 2'); - assert_ge(5_u16, 2_u16, '5 >= 2'); - assert(!(3_u16 > 3_u16), '!(3 > 3)'); - assert_ge(3_u16, 3_u16, '3 >= 3'); - assert_eq(@u16_sqrt(9), @3, 'u16_sqrt(9) == 3'); - assert_eq(@u16_sqrt(10), @3, 'u16_sqrt(10) == 3'); - assert_eq(@u16_sqrt(0x400), @0x20, 'u16_sqrt(2^10) == 2^5'); - assert_eq(@u16_sqrt(0xffff), @0xff, 'Wrong square root result.'); - assert_eq(@u16_sqrt(1), @1, 'u64_sqrt(1) == 1'); - assert_eq(@u16_sqrt(0), @0, 'u64_sqrt(0) == 0'); - assert_eq(@~0x0000_u16, @0xffff, '~0x0000 == 0xffff'); - assert_eq(@~0x8421_u16, @0x7bde, '~0x8421 == 0x7bde'); -} - -#[test] -#[should_panic] -fn test_u16_sub_overflow_1() { - 0_u16 - 1_u16; -} - -#[test] -#[should_panic] -fn test_u16_sub_overflow_2() { - 0_u16 - 3_u16; -} - -#[test] -#[should_panic] -fn test_u16_sub_overflow_3() { - 1_u16 - 3_u16; -} - -#[test] -#[should_panic] -fn test_u16_sub_overflow_4() { - 100_u16 - 250_u16; -} - -#[test] -#[should_panic] -fn test_u16_add_overflow_1() { - 0x8000_u16 + 0x8000_u16; -} - -#[test] -#[should_panic] -fn test_u16_add_overflow_2() { - 0x9000_u16 + 0x8001_u16; -} - -#[test] -#[should_panic] -fn test_u16_mul_overflow_1() { - 0x100_u16 * 0x100_u16; -} - -#[test] -#[should_panic] -fn test_u16_mul_overflow_2() { - 0x101_u16 * 0x100_u16; -} - -#[test] -#[should_panic] -fn test_u16_mul_overflow_3() { - 2_u16 * 0x8000_u16; -} - -#[test] -#[should_panic(expected: ('Division by 0',))] -fn test_u16_div_by_0() { - 2_u16 / 0_u16; -} - -#[test] -#[should_panic] -fn test_u16_mod_by_0() { - 0_u16 % 0_u16; -} - -#[test] -fn test_u32_operators() { - assert_eq(@1_u32, @1_u32, '1 == 1'); - assert_ne(@1_u32, @2_u32, '1 != 2'); - assert_eq(@(1_u32 + 3_u32), @4_u32, '1 + 3 == 4'); - assert_eq(@(3_u32 + 6_u32), @9_u32, '3 + 6 == 9'); - assert_eq(@(3_u32 - 1_u32), @2_u32, '3 - 1 == 2'); - assert_eq(@(231_u32 - 131_u32), @100_u32, '231-131=100'); - assert_eq(@(1_u32 * 3_u32), @3_u32, '1 * 3 == 3'); - assert_eq(@(2_u32 * 4_u32), @8_u32, '2 * 4 == 8'); - assert_eq(@(510670725_u32 / 7_u32), @72952960_u32, '510670725 / 7 == 72952960'); - assert_eq(@(510670725_u32 % 7_u32), @5_u32, '510670725 % 7 == 5'); - assert_eq(@((1_u32 | 2_u32)), @3_u32, '1 | 2 == 3'); - assert_eq(@((1_u32 & 2_u32)), @0_u32, '1 & 2 == 0'); - assert_eq(@((1_u32 ^ 2_u32)), @3_u32, '1 ^ 2 == 3'); - assert_eq(@((2_u32 | 2_u32)), @2_u32, '2 | 2 == 2'); - assert_eq(@((2_u32 & 2_u32)), @2_u32, '2 & 2 == 2'); - assert_eq(@((2_u32 & 3_u32)), @2_u32, '2 & 3 == 2'); - assert_eq(@((3_u32 ^ 6_u32)), @5_u32, '3 ^ 6 == 5'); - assert_lt(1_u32, 4_u32, '1 < 4'); - assert_le(1_u32, 4_u32, '1 <= 4'); - assert(!(4_u32 < 4_u32), '!(4 < 4)'); - assert_le(4_u32, 4_u32, '4 <= 4'); - assert_gt(5_u32, 2_u32, '5 > 2'); - assert_ge(5_u32, 2_u32, '5 >= 2'); - assert(!(3_u32 > 3_u32), '!(3 > 3)'); - assert_ge(3_u32, 3_u32, '3 >= 3'); - assert_eq(@u32_sqrt(9), @3, 'u32_sqrt(9) == 3'); - assert_eq(@u32_sqrt(10), @3, 'u32_sqrt(10) == 3'); - assert_eq(@u32_sqrt(0x100000), @0x400, 'u32_sqrt(2^20) == 2^10'); - assert_eq(@u32_sqrt(0xffffffff), @0xffff, 'Wrong square root result.'); - assert_eq(@u32_sqrt(1), @1, 'u64_sqrt(1) == 1'); - assert_eq(@u32_sqrt(0), @0, 'u64_sqrt(0) == 0'); - assert_eq(@~0x00000000_u32, @0xffffffff, '~0x00000000 == 0xffffffff'); - assert_eq(@~0x12345678_u32, @0xedcba987, '~0x12345678 == 0xedcba987'); -} - -#[test] -#[should_panic] -fn test_u32_sub_overflow_1() { - 0_u32 - 1_u32; -} - -#[test] -#[should_panic] -fn test_u32_sub_overflow_2() { - 0_u32 - 3_u32; -} - -#[test] -#[should_panic] -fn test_u32_sub_overflow_3() { - 1_u32 - 3_u32; -} - -#[test] -#[should_panic] -fn test_u32_sub_overflow_4() { - 100_u32 - 250_u32; -} - -#[test] -#[should_panic] -fn test_u32_add_overflow_1() { - 0x80000000_u32 + 0x80000000_u32; -} - -#[test] -#[should_panic] -fn test_u32_add_overflow_2() { - 0x90000000_u32 + 0x80000001_u32; -} - -#[test] -#[should_panic] -fn test_u32_mul_overflow_1() { - 0x10000_u32 * 0x10000_u32; -} - -#[test] -#[should_panic] -fn test_u32_mul_overflow_2() { - 0x10001_u32 * 0x10000_u32; -} - -#[test] -#[should_panic] -fn test_u32_mul_overflow_3() { - 2_u32 * 0x80000000_u32; -} - -#[test] -#[should_panic(expected: ('Division by 0',))] -fn test_u32_div_by_0() { - 2_u32 / 0_u32; -} - -#[test] -#[should_panic] -fn test_u32_mod_by_0() { - 0_u32 % 0_u32; -} - -#[test] -fn test_u64_operators() { - assert_eq(@1_u64, @1_u64, '1 == 1'); - assert_ne(@1_u64, @2_u64, '1 != 2'); - assert_eq(@(1_u64 + 3_u64), @4_u64, '1 + 3 == 4'); - assert_eq(@(3_u64 + 6_u64), @9_u64, '3 + 6 == 9'); - assert_eq(@(3_u64 - 1_u64), @2_u64, '3 - 1 == 2'); - assert_eq(@(231_u64 - 131_u64), @100_u64, '231-131=100'); - assert_eq(@(1_u64 * 3_u64), @3_u64, '1 * 3 == 3'); - assert_eq(@(2_u64 * 4_u64), @8_u64, '2 * 4 == 8'); - assert_eq( - @(5010670477878974275_u64 / 7_u64), @715810068268424896_u64, 'Wrong division result.' - ); - assert_eq(@(5010670477878974275_u64 % 7_u64), @3_u64, '5010670477878974275 % 7 == 3'); - assert_eq(@((1_u64 | 2_u64)), @3_u64, '1 | 2 == 3'); - assert_eq(@((1_u64 & 2_u64)), @0_u64, '1 & 2 == 0'); - assert_eq(@((1_u64 ^ 2_u64)), @3_u64, '1 ^ 2 == 3'); - assert_eq(@((2_u64 | 2_u64)), @2_u64, '2 | 2 == 2'); - assert_eq(@((2_u64 & 2_u64)), @2_u64, '2 & 2 == 2'); - assert_eq(@((2_u64 & 3_u64)), @2_u64, '2 & 3 == 2'); - assert_eq(@((3_u64 ^ 6_u64)), @5_u64, '3 ^ 6 == 5'); - assert_lt(1_u64, 4_u64, '1 < 4'); - assert_le(1_u64, 4_u64, '1 <= 4'); - assert(!(4_u64 < 4_u64), '!(4 < 4)'); - assert_le(4_u64, 4_u64, '4 <= 4'); - assert_gt(5_u64, 2_u64, '5 > 2'); - assert_ge(5_u64, 2_u64, '5 >= 2'); - assert(!(3_u64 > 3_u64), '!(3 > 3)'); - assert_ge(3_u64, 3_u64, '3 >= 3'); - assert_eq(@u64_sqrt(9), @3, 'u64_sqrt(9) == 3'); - assert_eq(@u64_sqrt(10), @3, 'u64_sqrt(10) == 3'); - assert_eq(@u64_sqrt(0x10000000000), @0x100000, 'u64_sqrt(2^40) == 2^20'); - assert_eq(@u64_sqrt(0xffffffffffffffff), @0xffffffff, 'Wrong square root result.'); - assert_eq(@u64_sqrt(1), @1, 'u64_sqrt(1) == 1'); - assert_eq(@u64_sqrt(0), @0, 'u64_sqrt(0) == 0'); - assert_eq(@~0x0000000000000000_u64, @0xffffffffffffffff, '~0x0..0 == 0xf..f'); - assert_eq(@~0x123456789abcdef1_u64, @0xedcba9876543210e, '~0x12..ef1 == 0xed..10e'); -} - -#[test] -#[should_panic] -fn test_u64_sub_overflow_1() { - 0_u64 - 1_u64; -} - -#[test] -#[should_panic] -fn test_u64_sub_overflow_2() { - 0_u64 - 3_u64; -} - -#[test] -#[should_panic] -fn test_u64_sub_overflow_3() { - 1_u64 - 3_u64; -} - -#[test] -#[should_panic] -fn test_u64_sub_overflow_4() { - 100_u64 - 250_u64; -} - -#[test] -#[should_panic] -fn test_u64_add_overflow_1() { - 0x8000000000000000_u64 + 0x8000000000000000_u64; -} - -#[test] -#[should_panic] -fn test_u64_add_overflow_2() { - 0x9000000000000000_u64 + 0x8000000000000001_u64; -} - -#[test] -#[should_panic] -fn test_u64_mul_overflow_1() { - 0x100000000_u64 * 0x100000000_u64; -} - -#[test] -#[should_panic] -fn test_u64_mul_overflow_2() { - 0x100000001_u64 * 0x100000000_u64; -} - -#[test] -#[should_panic] -fn test_u64_mul_overflow_3() { - 2_u64 * 0x8000000000000000_u64; -} - -#[test] -#[should_panic(expected: ('Division by 0',))] -fn test_u64_div_by_0() { - 2_u64 / 0_u64; -} - -#[test] -#[should_panic] -fn test_u64_mod_by_0() { - 0_u64 % 0_u64; -} - -#[test] -fn test_u128_operators() { - assert_eq(@1_u128, @1_u128, '1 == 1'); - assert_ne(@1_u128, @2_u128, '1 != 2'); - assert_eq(@(1_u128 + 3_u128), @4_u128, '1 + 3 == 4'); - assert_eq(@(3_u128 + 6_u128), @9_u128, '3 + 6 == 9'); - assert_eq(@(3_u128 - 1_u128), @2_u128, '3 - 1 == 2'); - assert_eq(@(1231_u128 - 231_u128), @1000_u128, '1231-231=1000'); - assert_eq(@(1_u128 * 3_u128), @3_u128, '1 * 3 == 3'); - assert_eq(@(2_u128 * 4_u128), @8_u128, '2 * 4 == 8'); - assert_eq(@(8_u128 / 2_u128), @4_u128, '8 / 2 == 4'); - assert_eq(@(8_u128 % 2_u128), @0_u128, '8 % 2 == 0'); - assert_eq(@(7_u128 / 3_u128), @2_u128, '7 / 3 == 2'); - assert_eq(@(7_u128 % 3_u128), @1_u128, '7 % 3 == 1'); - assert_lt(1_u128, 4_u128, '1 < 4'); - assert_le(1_u128, 4_u128, '1 <= 4'); - assert(!(4_u128 < 4_u128), '!(4 < 4)'); - assert_le(4_u128, 4_u128, '4 <= 4'); - assert_gt(5_u128, 2_u128, '5 > 2'); - assert_ge(5_u128, 2_u128, '5 >= 2'); - assert(!(3_u128 > 3_u128), '!(3 > 3)'); - assert_ge(3_u128, 3_u128, '3 >= 3'); - assert_eq(@((1_u128 | 2_u128)), @3_u128, '1 | 2 == 3'); - assert_eq(@((1_u128 & 2_u128)), @0_u128, '1 & 2 == 0'); - assert_eq(@((1_u128 ^ 2_u128)), @3_u128, '1 ^ 2 == 3'); - assert_eq(@((2_u128 | 2_u128)), @2_u128, '2 | 2 == 2'); - assert_eq(@((2_u128 & 2_u128)), @2_u128, '2 & 2 == 2'); - assert_eq(@((2_u128 & 3_u128)), @2_u128, '2 & 3 == 2'); - assert_eq(@((3_u128 ^ 6_u128)), @5_u128, '3 ^ 6 == 5'); - assert_eq(@u128_sqrt(9), @3, 'u128_sqrt(9) == 3'); - assert_eq(@u128_sqrt(10), @3, 'u128_sqrt(10) == 3'); - assert_eq( - @u128_sqrt(0x10000000000000000000000000), @0x4000000000000, 'u128_sqrt(2^100) == 2^50' - ); - assert_eq( - @u128_sqrt(0xffffffffffffffffffffffffffffffff), - @0xffffffffffffffff, - 'Wrong square root result.' - ); - assert_eq(@u128_sqrt(1), @1, 'u128_sqrt(1) == 1'); - assert_eq(@u128_sqrt(0), @0, 'u128_sqrt(0) == 0'); - assert_eq( - @~0x00000000000000000000000000000000_u128, - @0xffffffffffffffffffffffffffffffff, - '~0x0..0 == 0xf..f' - ); - assert_eq( - @~0x123456789abcdef123456789abcdef12_u128, - @0xedcba9876543210edcba9876543210ed, - '~0x12..ef12 == 0xed..10ed' - ); -} - -fn pow_2_64() -> u128 { - 0x10000000000000000_u128 -} - -#[test] -#[should_panic] -fn test_u128_sub_overflow_1() { - 0_u128 - 1_u128; -} - -#[test] -#[should_panic] -fn test_u128_sub_overflow_2() { - 0_u128 - 3_u128; -} - -#[test] -#[should_panic] -fn test_u128_sub_overflow_3() { - 1_u128 - 3_u128; -} - -#[test] -#[should_panic] -fn test_u128_sub_overflow_4() { - 100_u128 - 1000_u128; -} - -#[test] -fn test_u128_wrapping_sub_1() { - let max_u128: u128 = BoundedInt::max(); - let should_be_max = u128_wrapping_sub(0_u128, 1_u128); - assert_eq(@max_u128, @should_be_max, 'Should be max u128') -} - -#[test] -fn test_u128_wrapping_sub_2() { - let max_u128_minus_two: u128 = BoundedInt::max() - 2; - let should_be_max = u128_wrapping_sub(0_u128, 3_u128); - assert_eq(@max_u128_minus_two, @should_be_max, 'Should be max u128 - 2') -} - -#[test] -fn test_u128_wrapping_sub_3() { - let max_u128_minus_899: u128 = BoundedInt::max() - 899; - let should_be_max = u128_wrapping_sub(100, 1000); - assert_eq(@max_u128_minus_899, @should_be_max, 'Should be max u128 - 899') -} - -#[test] -fn test_u128_wrapping_sub_4() { - let should_be_zero = u128_wrapping_sub(0_u128, 0_u128); - assert_eq(@should_be_zero, @0, 'Should be 0') -} - -#[test] -#[should_panic] -fn test_u128_add_overflow_1() { - 0x80000000000000000000000000000000_u128 + 0x80000000000000000000000000000000_u128; -} - -#[test] -#[should_panic] -fn test_u128_add_overflow_2() { - (0x80000000000000000000000000000000_u128 + 12_u128) + 0x80000000000000000000000000000000_u128; -} - -#[test] -#[should_panic] -fn test_u128_mul_overflow_1() { - pow_2_64() * pow_2_64(); -} - -#[test] -#[should_panic] -fn test_u128_mul_overflow_2() { - (pow_2_64() + 1_u128) * pow_2_64(); -} - -#[test] -#[should_panic] -fn test_u128_mul_overflow_3() { - 2_u128 * 0x80000000000000000000000000000000_u128; -} - -#[test] -#[should_panic(expected: ('Division by 0',))] -fn test_u128_div_by_0() { - 2_u128 / 0_u128; -} - -#[test] -#[should_panic] -fn test_u128_mod_by_0() { - 2_u128 % 0_u128; -} - -fn pow_2_127() -> u256 { - 0x80000000000000000000000000000000_u256 -} - -#[test] -fn test_u256_from_felt252() { - assert_eq(@1.into(), @1_u256, 'into 1'); - assert_eq( - @(170141183460469231731687303715884105728 * 2).into(), - @0x100000000000000000000000000000000_u256, - 'into 2**128' - ); -} - -#[test] -fn test_u256_operators() { - let max_u128 = 0xffffffffffffffffffffffffffffffff_u256; - assert_eq( - @(0x100000000000000000000000000000001 + 0x300000000000000000000000000000002), - @0x400000000000000000000000000000003_u256, - 'no Overflow' - ); - assert_eq( - @(0x180000000000000000000000000000000 + 0x380000000000000000000000000000000), - @0x500000000000000000000000000000000_u256, - 'basic Overflow' - ); - assert_eq( - @(0x400000000000000000000000000000003 - 0x100000000000000000000000000000001), - @0x300000000000000000000000000000002_u256, - 'no UF' - ); - assert_eq( - @(0x500000000000000000000000000000000 - 0x180000000000000000000000000000000), - @0x380000000000000000000000000000000_u256, - 'basic UF' - ); - assert_eq( - @(0x400000000000000000000000000000003 * 1), - @0x400000000000000000000000000000003_u256, - 'mul by 1' - ); - assert_eq( - @(0x400000000000000000000000000000003 * 2), - @0x800000000000000000000000000000006_u256, - 'mul by 2' - ); - assert_eq( - @(0x80000000000000000000000000000000 * 2), - @0x100000000000000000000000000000000_u256, - 'basic mul Overflow' - ); - assert_eq( - @(max_u128 * max_u128), - @0xfffffffffffffffffffffffffffffffe00000000000000000000000000000001_u256, - 'max_u128 * max_u128' - ); - assert_eq(@(max_u128 * 1), @max_u128, 'max_u128 * 1'); - assert_eq(@(1 * max_u128), @max_u128, '1 * max_u128'); - let v0_2: u256 = 0x000000000000000000000000000000002; - let v0_3: u256 = 0x000000000000000000000000000000003; - let v1_1: u256 = 0x100000000000000000000000000000001; - let v1_2: u256 = 0x100000000000000000000000000000002; - let v2_0: u256 = 0x200000000000000000000000000000000; - let v2_1: u256 = 0x200000000000000000000000000000001; - let v2_2: u256 = 0x200000000000000000000000000000002; - let v2_3: u256 = 0x200000000000000000000000000000003; - let v3_0: u256 = 0x300000000000000000000000000000000; - let v3_2: u256 = 0x300000000000000000000000000000002; - assert_eq(@(v1_2 | v2_2), @v3_2, '1.2|2.2==3.2'); - assert_eq(@(v2_1 | v2_2), @v2_3, '2.1|2.2==2.3'); - assert_eq(@(v2_2 | v1_2), @v3_2, '2.2|1.2==3.2'); - assert_eq(@(v2_2 | v2_1), @v2_3, '2.2|2.1==2.3'); - assert_eq(@(v1_2 & v2_2), @v0_2, '1.2&2.2==0.2'); - assert_eq(@(v2_1 & v2_2), @v2_0, '2.1&2.2==2.0'); - assert_eq(@(v2_2 & v1_2), @v0_2, '2.2&1.2==0.2'); - assert_eq(@(v2_2 & v2_1), @v2_0, '2.2&2.1==2.0'); - assert_eq(@(v1_2 ^ v2_2), @v3_0, '1.2^2.2==3.0'); - assert_eq(@(v2_1 ^ v2_2), @v0_3, '2.1^2.2==0.3'); - assert_eq(@(v2_2 ^ v1_2), @v3_0, '2.2^1.2==3.0'); - assert_eq(@(v2_2 ^ v2_1), @v0_3, '2.2^2.1==0.3'); - assert_lt(v1_2, v2_2, '1.2<2.2'); - assert_lt(v2_1, v2_2, '2.1<2.2'); - assert(!(v2_2 < v1_2), '2.2<1.2'); - assert(!(v2_2 < v2_1), '2.2<2.1'); - assert(!(v2_2 < v2_2), '2.2<2.2'); - assert_le(v1_2, v2_2, '1.2<=2.2'); - assert_le(v2_1, v2_2, '2.1<=2.2'); - assert(!(v2_2 <= v1_2), '2.2<=1.2'); - assert(!(v2_2 <= v2_1), '2.2<=2.1'); - assert_le(v2_2, v2_2, '2.2<=2.2'); - assert(!(v1_2 > v2_2), '1.2>2.2'); - assert(!(v2_1 > v2_2), '2.1>2.2'); - assert_gt(v2_2, v1_2, '2.2>1.2'); - assert_gt(v2_2, v2_1, '2.2>2.1'); - assert(!(v2_2 > v2_2), '2.2>2.2'); - assert(!(v1_2 >= v2_2), '1.2>=2.2'); - assert(!(v2_1 >= v2_2), '2.1>=2.2'); - assert_ge(v2_2, v1_2, '2.2>=1.2'); - assert_ge(v2_2, v2_1, '2.2>=2.1'); - assert_ge(v2_2, v2_2, '2.2>=2.2'); - - assert_eq(@(v3_2 / v1_1), @v0_2, 'u256 div'); - assert_eq( - @(0x400000000000000000000000000000002 / 3), - @0x155555555555555555555555555555556_u256, - 'u256 div' - ); - assert_eq(@(0x400000000000000000000000000000002 % 3), @0_u256, 'u256 mod'); - assert_eq(@(0x10000000000000000 / 0x10000000000000000), @1_u256, 'u256 div'); - assert_eq(@(0x10000000000000000 % 0x10000000000000000), @0_u256, 'u256 mod'); - assert_eq( - @(0x1000000000000000000000000000000000000000000000000 - / 0x1000000000000000000000000000000000000000000000000), - @1_u256, - 'u256 div' - ); - assert_eq( - @(0x1000000000000000000000000000000000000000000000000 % 0x1000000000000000000000000000000000000000000000000), - @0_u256, - 'u256 mod' - ); - assert_eq(@(BoundedInt::max() % 0x100000000), @0xffffffff_u256, 'u256 mod'); - assert_eq(@(BoundedInt::max() % 0x10000000000000000), @0xffffffffffffffff_u256, 'u256 mod'); - assert_eq( - @(BoundedInt::max() / 0x10000000000000000000000000000000000000000), - @0xffffffffffffffffffffffff_u256, - 'u256 div' - ); - assert_eq( - @(BoundedInt::max() / 0x1000000000000000000000000000000000000000000000000), - @0xffffffffffffffff_u256, - 'u256 div' - ); - assert_eq( - @~max_u128, - @0xffffffffffffffffffffffffffffffff00000000000000000000000000000000, - '~0x0..0f..f == 0xf..f0..0' - ); - assert_eq( - @~0xffffffffffffffffffffffffffffffff00000000000000000000000000000000, - @max_u128, - '~0xf..f0..0 == 0x0..0f..f' - ); -} - -#[test] -#[should_panic] -fn test_u256_add_overflow() { - let v = 0x8000000000000000000000000000000000000000000000000000000000000001_u256; - v + v; -} - -#[test] -#[should_panic] -fn test_u256_sub_overflow() { - 0x100000000000000000000000000000001_u256 - 0x100000000000000000000000000000002; -} - -#[test] -#[should_panic] -fn test_u256_mul_overflow_1() { - 0x100000000000000000000000000000001_u256 * 0x100000000000000000000000000000002; -} - -#[test] -#[should_panic] -fn test_u256_mul_overflow_2() { - pow_2_127() * 0x200000000000000000000000000000000; -} - -#[test] -fn test_u256_wide_mul() { - assert_eq(@u256_wide_mul(0, 0), @u512 { limb0: 0, limb1: 0, limb2: 0, limb3: 0 }, '0 * 0 != 0'); - assert_eq( - @u256_wide_mul( - 0x1001001001001001001001001001001001001001001001001001, - 0x1000100010001000100010001000100010001000100010001000100010001 - ), - @u512 { - limb0: 0x33233223222222122112111111011001, - limb1: 0x54455445544554454444443443343333, - limb2: 0x21222222322332333333433443444444, - limb3: 0x1001101111112112 - }, - 'long calculation failed' - ); -} - -#[test] -fn test_u512_safe_div_rem_by_u256() { - let zero = u512 { limb0: 0, limb1: 0, limb2: 0, limb3: 0 }; - let one = u512 { limb0: 1, limb1: 0, limb2: 0, limb3: 0 }; - let large_num = u512 { - limb0: 0x33233223222222122112111111011001, - limb1: 0x54455445544554454444443443343333, - limb2: 0x21222222322332333333433443444444, - limb3: 0x1001101111112112 - }; - let (q, r) = u512_safe_div_rem_by_u256(zero, 1_u256.try_into().unwrap()); - assert(q == zero, '0 / 1 != 0'); - assert(r == 0, '0 % 1 != 0'); - let (q, r) = u512_safe_div_rem_by_u256(one, 1_u256.try_into().unwrap()); - assert(q == one, '1 / 1 != 1'); - assert(r == 0, '1 % 1 != 0'); - let (q, r) = u512_safe_div_rem_by_u256(large_num, 1_u256.try_into().unwrap()); - assert(q == large_num, 'LARGE / 1 != LARGE'); - assert(r == 0, 'LARGE % 1 != 0'); - let (q, r) = u512_safe_div_rem_by_u256( - large_num, 0x33233223222222122112111111011001_u256.try_into().unwrap() - ); - assert( - q == u512 { - limb0: 0x365ec98ac1c2c57afaff780a20a0b2b1, - limb1: 0xf3dfa68ede27c4236ef0c6eb66a8e0a2, - limb2: 0x501e5b7ba7f4ec12, - limb3: 0 - }, - 'large div failed' - ); - assert(r == 0x1e0eb905027d0150d2618bbd71844d50, 'large rem failed'); -} - -#[test] -fn test_min() { - let min_u8: u8 = BoundedInt::min(); - let min_u16: u16 = BoundedInt::min(); - let min_u32: u32 = BoundedInt::min(); - let min_u64: u64 = BoundedInt::min(); - let min_u128: u128 = BoundedInt::min(); - let min_u256: u256 = BoundedInt::min(); - assert_eq(@min_u8, @0_u8, 'not zero'); - assert_eq(@min_u16, @0_u16, 'not zero'); - assert_eq(@min_u32, @0_u32, 'not zero'); - assert_eq(@min_u64, @0_u64, 'not zero'); - assert_eq(@min_u128, @0_u128, 'not zero'); - assert_eq(@min_u256, @0_u256, 'not zero'); -} - -#[test] -fn test_max() { - let max_u8: u8 = BoundedInt::max(); - let max_u16: u16 = BoundedInt::max(); - let max_u32: u32 = BoundedInt::max(); - let max_u64: u64 = BoundedInt::max(); - let max_u128: u128 = BoundedInt::max(); - let max_u256: u256 = BoundedInt::max(); - assert_eq(@max_u8, @0xff_u8, 'not max'); - assert_eq(@max_u16, @0xffff_u16, 'not max'); - assert_eq(@max_u32, @0xffffffff_u32, 'not max'); - assert_eq(@max_u64, @0xffffffffffffffff_u64, 'not max'); - assert_eq(@max_u128, @0xffffffffffffffffffffffffffffffff_u128, 'not max'); - assert_eq( - @max_u256, - @0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256, - 'not max' - ); -} - -#[test] -#[should_panic] -fn test_max_u8_plus_1_overflow() { - BoundedInt::max() + 1_u8; -} - -#[test] -#[should_panic] -fn test_max_u16_plus_1_overflow() { - BoundedInt::max() + 1_u16; -} - -#[test] -#[should_panic] -fn test_max_u32_plus_1_overflow() { - BoundedInt::max() + 1_u32; -} -#[test] -#[should_panic] -fn test_max_u64_plus_1_overflow() { - BoundedInt::max() + 1_u64; -} - -#[test] -#[should_panic] -fn test_max_u128_plus_1_overflow() { - BoundedInt::max() + 1_u128; -} - -#[test] -#[should_panic] -fn test_max_u256_plus_1_overflow() { - BoundedInt::max() + Into::::into(1); -} - -#[test] -fn test_default_values() { - assert_eq(@Default::default(), @0, '0 == 0'); - assert_eq(@Default::default(), @0_u8, '0 == 0'); - assert_eq(@Default::default(), @0_u16, '0 == 0'); - assert_eq(@Default::default(), @0_u32, '0 == 0'); - assert_eq(@Default::default(), @0_u64, '0 == 0'); - assert_eq(@Default::default(), @0_u128, '0 == 0'); - assert_eq(@Default::default(), @0_u256, '0 == 0'); -} - -#[test] -fn test_default_felt252dict_values() { - assert_eq(@Felt252DictValue::zero_default(), @0, '0 == 0'); - assert_eq(@Felt252DictValue::zero_default(), @0_u8, '0 == 0'); - assert_eq(@Felt252DictValue::zero_default(), @0_u16, '0 == 0'); - assert_eq(@Felt252DictValue::zero_default(), @0_u32, '0 == 0'); - assert_eq(@Felt252DictValue::zero_default(), @0_u64, '0 == 0'); - assert_eq(@Felt252DictValue::zero_default(), @0_u128, '0 == 0'); -} - -#[test] -fn test_u256_sqrt() { - assert_eq(@u256_sqrt(9.into()), @3, 'u256_sqrt(9) == 3'); - assert_eq(@u256_sqrt(10.into()), @3, 'u256_sqrt(10) == 3'); - assert_eq( - @u256_sqrt(1267650600228229401496703205376.into()), - @1125899906842624, - 'u256_sqrt(2^100) == 2^50' - ); - assert_eq( - @u256_sqrt(340282366920938463463374607431768211455.into()), - @18446744073709551615, - 'Wrong square root result.' - ); - assert_eq(@u256_sqrt(1.into()), @1, 'u256_sqrt(1) == 1'); - assert_eq(@u256_sqrt(0.into()), @0, 'u256_sqrt(0) == 0'); - - assert_eq(@u256_sqrt(BoundedInt::max()), @BoundedInt::max(), 'u256::MAX**0.5==u128::MAX'); - let (high, low) = integer::u128_wide_mul(BoundedInt::max(), BoundedInt::max()); - assert_eq(@u256_sqrt(u256 { low, high }), @BoundedInt::max(), '(u128::MAX**2)**0.5==u128::MAX'); -} - -#[test] -fn test_u256_try_into_felt252() { - assert_eq(@1_u256.try_into().unwrap(), @1_felt252, '1 == 1'_felt252); - assert_eq( - @0x800000000000011000000000000000000000000000000000000000000000000_u256.try_into().unwrap(), - @0x800000000000011000000000000000000000000000000000000000000000000_felt252, - 'P-1 == P-1'_felt252 - ); - assert_eq( - @0x800000000000010ffffffffffffffffffffffffffffffffffffffffffffffff_u256.try_into().unwrap(), - @0x800000000000010ffffffffffffffffffffffffffffffffffffffffffffffff_felt252, - 'P-2 == P-2'_felt252 - ); - let f: Option = 0x800000000000011000000000000000000000000000000000000000000000001_u256 - .try_into(); - assert(f.is_none(), 'prime is not felt252'); - let f: Option = 0x800000000000011000000000000000000000000000000000000000000000002_u256 - .try_into(); - assert(f.is_none(), 'prime+1 is not felt252'); - let f: Option = 0x800000000000011000000000000000100000000000000000000000000000001_u256 - .try_into(); - assert(f.is_none(), 'prime+2**128 is not felt252'); -} - -/// Checks if `b` is out of range of `A`. -fn is_out_of_range, +TryInto>(b: B) -> bool { - let no_a: Option = b.try_into(); - no_a.is_none() -} - -/// Checks if `SubType` is trivially castable to `SuperType`. -fn cast_subtype_valid< - SubType, - SuperType, - +Drop, - +Drop, - +Copy, - +Copy, - +BoundedInt, - +PartialEq, - +PartialEq, - +Into, - +TryInto ->() -> bool { - let max_sub: SubType = BoundedInt::max(); - let max_sub_as_super: SuperType = max_sub.into(); - let min_sub: SubType = BoundedInt::min(); - let min_sub_as_super: SuperType = min_sub.into(); - min_sub_as_super.try_into().unwrap() == min_sub - && max_sub_as_super.try_into().unwrap() == max_sub -} - -/// Checks that `A::max()` is castable to `B`, and `A::max() + 1` is in `B`s range, and not -/// castable back to `A`. -fn validate_max_strictly_contained< - A, - B, - +Drop, - +Drop, - +Copy, - +Copy, - +Add, - +BoundedInt, - +PartialEq, - +PartialEq, - +TryInto, - +TryInto, - +TryInto ->( - err: felt252 -) { - let max_a: A = BoundedInt::max(); - let max_a_as_b: B = max_a.try_into().expect(err); - assert(Option::Some(max_a) == max_a_as_b.try_into(), err); - assert(is_out_of_range::(max_a_as_b + 1.try_into().unwrap()), err); -} - -/// Checks that `A::min()` is castable to `B`, and `A::min() - 1` is in `B`s range, and not -/// castable back to `A`. -fn validate_min_strictly_contained< - A, - B, - +Drop, - +Drop, - +Copy, - +Copy, - +Sub, - +BoundedInt, - +PartialEq, - +PartialEq, - +TryInto, - +TryInto, - +TryInto ->( - err: felt252 -) { - let min_sub: A = BoundedInt::min(); - let min_sub_as_super: B = min_sub.try_into().expect(err); - assert(Option::Some(min_sub) == min_sub_as_super.try_into(), err); - assert(is_out_of_range::(min_sub_as_super - 1.try_into().unwrap()), err); -} - -/// Checks that castings from `SubType` to `SuperType` are correct around the bounds, where -/// `SubType` is strictly contained (in both bounds) in `SuperType`. -fn validate_cast_bounds_strictly_contained< - SubType, - SuperType, - +Drop, - +Drop, - +Copy, - +Copy, - +Add, - +Sub, - +BoundedInt, - +PartialEq, - +PartialEq, - +Into, - +TryInto, - +TryInto ->( - err: felt252 -) { - assert(cast_subtype_valid::(), err); - validate_min_strictly_contained::(err); - validate_max_strictly_contained::(err); -} - -/// Checks that castings from `SubType` to `SuperType` are correct around the bounds, where -/// `SubType` has the same min as `SuperType`, but has a lower max. -fn validate_cast_bounds_contained_same_min< - SubType, - SuperType, - +Drop, - +Drop, - +Copy, - +Copy, - +Add, - +Sub, - +BoundedInt, - +BoundedInt, - +PartialEq, - +PartialEq, - +Into, - +TryInto, - +TryInto ->( - err: felt252 -) { - assert(cast_subtype_valid::(), err); - assert(BoundedInt::::min().into() == BoundedInt::::min(), err); - validate_max_strictly_contained::(err); -} - -/// Checks that castings from `A` to `B` are correct around the bounds. -/// Assumes that the ordering of the bounds is: `a_min < b_min < a_max < b_max`. -fn validate_cast_bounds_overlapping< - A, - B, - +Drop, - +Drop, - +Copy, - +Copy, - +Sub, - +Add, - +BoundedInt, - +BoundedInt, - +PartialEq, - +PartialEq, - +TryInto, - +TryInto, - +TryInto, - +TryInto ->( - err: felt252 -) { - validate_min_strictly_contained::(err); - validate_max_strictly_contained::(err); -} - -#[test] -fn proper_cast() { - validate_cast_bounds_contained_same_min::('u8 u16 casts'); - validate_cast_bounds_contained_same_min::('u8 u32 casts'); - validate_cast_bounds_contained_same_min::('u8 u64 casts'); - validate_cast_bounds_contained_same_min::('u8 u128 casts'); - validate_cast_bounds_contained_same_min::('u16 u32 casts'); - validate_cast_bounds_contained_same_min::('u16 u64 casts'); - validate_cast_bounds_contained_same_min::('u16 u128 casts'); - validate_cast_bounds_contained_same_min::('u32 u64 casts'); - validate_cast_bounds_contained_same_min::('u32 u128 casts'); - validate_cast_bounds_contained_same_min::('u64 u128 casts'); - - validate_cast_bounds_strictly_contained::('u8 i16 casts'); - validate_cast_bounds_strictly_contained::('u8 i32 casts'); - validate_cast_bounds_strictly_contained::('u8 i64 casts'); - validate_cast_bounds_strictly_contained::('u8 i128 casts'); - validate_cast_bounds_strictly_contained::('u16 i32 casts'); - validate_cast_bounds_strictly_contained::('u16 i64 casts'); - validate_cast_bounds_strictly_contained::('u16 i128 casts'); - validate_cast_bounds_strictly_contained::('u32 i64 casts'); - validate_cast_bounds_strictly_contained::('u32 i128 casts'); - validate_cast_bounds_strictly_contained::('u64 i128 casts'); - - validate_cast_bounds_strictly_contained::('i8 i16 casts'); - validate_cast_bounds_strictly_contained::('i8 i32 casts'); - validate_cast_bounds_strictly_contained::('i8 i64 casts'); - validate_cast_bounds_strictly_contained::('i8 i128 casts'); - validate_cast_bounds_strictly_contained::('i16 i32 casts'); - validate_cast_bounds_strictly_contained::('i16 i64 casts'); - validate_cast_bounds_strictly_contained::('i16 i128 casts'); - validate_cast_bounds_strictly_contained::('i32 i64 casts'); - validate_cast_bounds_strictly_contained::('i32 i128 casts'); - validate_cast_bounds_strictly_contained::('i64 i128 casts'); - - validate_cast_bounds_overlapping::('i8 u8 casts'); - validate_cast_bounds_overlapping::('i8 u16 casts'); - validate_cast_bounds_overlapping::('i8 u32 casts'); - validate_cast_bounds_overlapping::('i8 u64 casts'); - validate_cast_bounds_overlapping::('i8 u128 casts'); - validate_cast_bounds_overlapping::('i16 u16 casts'); - validate_cast_bounds_overlapping::('i16 u32 casts'); - validate_cast_bounds_overlapping::('i16 u64 casts'); - validate_cast_bounds_overlapping::('i16 u128 casts'); - validate_cast_bounds_overlapping::('i32 u32 casts'); - validate_cast_bounds_overlapping::('i32 u64 casts'); - validate_cast_bounds_overlapping::('i32 u128 casts'); - validate_cast_bounds_overlapping::('i64 u64 casts'); - validate_cast_bounds_overlapping::('i64 u128 casts'); - validate_cast_bounds_overlapping::('i128 u128 casts'); -} - -#[test] -fn test_into_self_type() { - assert_eq(@0xFF_u8.into(), @0xFF_u8, 'u8 into u8'); - assert_eq(@0xFFFF_u16.into(), @0xFFFF_u16, 'u16 into u16'); - assert_eq(@0xFFFFFFFF_u32.into(), @0xFFFFFFFF_u32, 'u32 into u32'); - assert_eq(@0xFFFFFFFFFFFFFFFF_u64.into(), @0xFFFFFFFFFFFFFFFF_u64, 'u64 into u64'); - assert_eq( - @0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_u128.into(), - @0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_u128, - 'u128 into u128' - ); - assert_eq( - @u256 { low: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, high: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF } - .into(), - @u256 { high: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, low: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF }, - 'u256 into u256' - ); -} - -#[test] -#[should_panic] -fn panic_u16_u8_1() { - let _out: u8 = (0xFF_u16 + 1_u16).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u16_u8_2() { - let max_u16: u16 = 0xFFFF; - let _out: u8 = max_u16.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u32_u8_1() { - let _out: u8 = (0xFF_u32 + 1_u32).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u32_u8_2() { - let max_u32: u32 = 0xFFFFFFFF; - let _out: u8 = max_u32.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u64_u8_1() { - let _out: u8 = (0xFF_u64 + 1_u64).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u64_u8_2() { - let max_u64: u64 = 0xFFFFFFFFFFFFFFFF; - let _out: u8 = max_u64.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u128_u8_1() { - let _out: u8 = (0xFF_u128 + 1_u128).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u8_2() { - let max_u128: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - let _out: u8 = max_u128.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u32_u16_1() { - let _out: u16 = (0xFFFF_u32 + 1_u32).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u32_u16_2() { - let max_u32: u32 = 0xFFFFFFFF; - let _out: u16 = max_u32.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u64_u16_1() { - let _out: u16 = (0xFFFF_u64 + 1_u64).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u64_u16_2() { - let max_u64: u64 = 0xFFFFFFFFFFFFFFFF; - let _out: u16 = max_u64.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u128_u16_1() { - let _out: u16 = (0xFFFF_u128 + 1_u128).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u16_2() { - let max_u128: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - let _out: u16 = max_u128.try_into().unwrap(); -} -#[test] -#[should_panic] -fn panic_u64_u32_1() { - let _out: u32 = (0xFFFFFFFF_u64 + 1_u64).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u64_u32_2() { - let max_u64: u64 = 0xFFFFFFFFFFFFFFFF; - let _out: u32 = max_u64.try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u32_1() { - let _out: u32 = (0xFFFFFFFF_u128 + 1_u128).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u32_2() { - let max_u128: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - let _out: u32 = max_u128.try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u64_1() { - let _out: u64 = (0xFFFFFFFFFFFFFFFF_u128 + 1_u128).try_into().unwrap(); -} - -#[test] -#[should_panic] -fn panic_u128_u64_2() { - let max_u128: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - let _out: u64 = max_u128.try_into().unwrap(); -} - -#[test] -fn test_u128_byte_reverse() { - assert_eq( - @integer::u128_byte_reverse(0x000102030405060708090a0b0c0d0e0f), - @0x0f0e0d0c0b0a09080706050403020100, - 'Wrong byte reverse' - ); -} - -#[test] -fn test_i8_operators() { - assert_eq(@1_i8, @1_i8, '1 == 1'); - assert_ne(@1_i8, @2_i8, '1 != 2'); - assert_eq(@0x7f_felt252.try_into().unwrap(), @0x7f_i8, '0x7f is not i8'); - let v: Option = 0x80_felt252.try_into(); - assert(v.is_none(), '0x80 is i8'); - assert_eq(@(-0x80_felt252).try_into().unwrap(), @-0x80_i8, '-0x80 is not i8'); - let v: Option = (-0x81_felt252).try_into(); - assert(v.is_none(), '-0x81 is i8'); - assert_eq(@(1_i8 + 3_i8), @4_i8, '1 + 3 == 4'); - assert_eq(@(3_i8 + 6_i8), @9_i8, '3 + 6 == 9'); - assert_eq(@(3_i8 - 1_i8), @2_i8, '3 - 1 == 2'); - assert_eq(@(121_i8 - 21_i8), @100_i8, '121-21=100'); - assert_eq(@(-1_i8 + -3_i8), @-4_i8, '-1 + -3 == -4'); - assert_eq(@(-3_i8 + -6_i8), @-9_i8, '-3 + -6 == -9'); - assert_eq(@(-3_i8 - -1_i8), @-2_i8, '-3 - -1 == -2'); - assert_eq(@(-121_i8 - -21_i8), @-100_i8, '-121--21=-100'); - assert_eq(@(1_i8 * 3_i8), @3_i8, '1 * 3 == 3'); - assert_eq(@(2_i8 * 4_i8), @8_i8, '2 * 4 == 8'); - assert_eq(@(-1_i8 * 3_i8), @-3_i8, '-1 * 3 == 3'); - assert_eq(@(-2_i8 * 4_i8), @-8_i8, '-2 * 4 == 8'); - assert_eq(@(1_i8 * -3_i8), @-3_i8, '1 * -3 == -3'); - assert_eq(@(2_i8 * -4_i8), @-8_i8, '2 * -4 == -8'); - assert_eq(@(-1_i8 * -3_i8), @3_i8, '-1 * -3 == 3'); - assert_eq(@(-2_i8 * -4_i8), @8_i8, '-2 * -4 == 8'); - assert_lt(1_i8, 4_i8, '1 < 4'); - assert_le(1_i8, 4_i8, '1 <= 4'); - assert(!(4_i8 < 4_i8), '!(4 < 4)'); - assert_le(5_i8, 5_i8, '5 <= 5'); - assert(!(5_i8 <= 4_i8), '!(5 <= 8)'); - assert_gt(5_i8, 2_i8, '5 > 2'); - assert_ge(5_i8, 2_i8, '5 >= 2'); - assert(!(3_i8 > 3_i8), '!(3 > 3)'); - assert_ge(3_i8, 3_i8, '3 >= 3'); -} - -#[test] -#[should_panic(expected: ('i8_sub Underflow',))] -fn test_i8_sub_underflow_1() { - -0x80_i8 - 1_i8; -} - -#[test] -#[should_panic(expected: ('i8_sub Underflow',))] -fn test_i8_sub_underflow_2() { - -0x80_i8 - 3_i8; -} - -#[test] -#[should_panic(expected: ('i8_sub Underflow',))] -fn test_i8_sub_underflow_3() { - -0x7f_i8 - 3_i8; -} - -#[test] -#[should_panic(expected: ('i8_sub Underflow',))] -fn test_i8_sub_underflow_4() { - -0x32_i8 - 0x7d_i8; -} - -#[test] -#[should_panic(expected: ('i8_sub Overflow',))] -fn test_i8_sub_overflow() { - 0x32_i8 - -0x7d_i8; -} - -#[test] -#[should_panic(expected: ('i8_add Overflow',))] -fn test_i8_add_overflow_1() { - 0x40_i8 + 0x40_i8; -} - -#[test] -#[should_panic(expected: ('i8_add Overflow',))] -fn test_i8_add_overflow_2() { - 0x64_i8 + 0x1e_i8; -} - -#[test] -#[should_panic(expected: ('i8_add Underflow',))] -fn test_i8_add_underflow() { - -0x64_i8 + -0x1e_i8; -} - -#[test] -#[should_panic] -fn test_i8_mul_overflow_1() { - 0x10_i8 * 0x10_i8; -} - -#[test] -#[should_panic] -fn test_i8_mul_overflow_2() { - 0x11_i8 * 0x10_i8; -} - -#[test] -#[should_panic] -fn test_i8_mul_overflow_3() { - 2_i8 * 0x40_i8; -} - -#[test] -fn test_i16_operators() { - assert_eq(@1_i16, @1_i16, '1 == 1'); - assert_ne(@1_i16, @2_i16, '1 != 2'); - assert_eq(@0x7fff_felt252.try_into().unwrap(), @0x7fff_i16, '0x7fff is not i16'); - let v: Option = 0x8000_felt252.try_into(); - assert(v.is_none(), '0x8000 is i16'); - assert_eq(@(-0x8000_felt252).try_into().unwrap(), @-0x8000_i16, '-0x8000 is not i16'); - let v: Option = (-0x8001_felt252).try_into(); - assert(v.is_none(), '-0x8001 is i16'); - assert_eq(@(1_i16 + 3_i16), @4_i16, '1 + 3 == 4'); - assert_eq(@(3_i16 + 6_i16), @9_i16, '3 + 6 == 9'); - assert_eq(@(3_i16 - 1_i16), @2_i16, '3 - 1 == 2'); - assert_eq(@(231_i16 - 131_i16), @100_i16, '231-131=100'); - assert_eq(@(-1_i16 + -3_i16), @-4_i16, '-1 + -3 == -4'); - assert_eq(@(-3_i16 + -6_i16), @-9_i16, '-3 + -6 == -9'); - assert_eq(@(-3_i16 - -1_i16), @-2_i16, '-3 - -1 == -2'); - assert_eq(@(-231_i16 - -131_i16), @-100_i16, '-231--131=-100'); - assert_eq(@(1_i16 * 3_i16), @3_i16, '1 * 3 == 3'); - assert_eq(@(2_i16 * 4_i16), @8_i16, '2 * 4 == 8'); - assert_eq(@(-1_i16 * 3_i16), @-3_i16, '-1 * 3 == 3'); - assert_eq(@(-2_i16 * 4_i16), @-8_i16, '-2 * 4 == 8'); - assert_eq(@(1_i16 * -3_i16), @-3_i16, '1 * -3 == -3'); - assert_eq(@(2_i16 * -4_i16), @-8_i16, '2 * -4 == -8'); - assert_eq(@(-1_i16 * -3_i16), @3_i16, '-1 * -3 == 3'); - assert_eq(@(-2_i16 * -4_i16), @8_i16, '-2 * -4 == 8'); - assert_lt(1_i16, 4_i16, '1 < 4'); - assert_le(1_i16, 4_i16, '1 <= 4'); - assert(!(4_i16 < 4_i16), '!(4 < 4)'); - assert_le(5_i16, 5_i16, '5 <= 5'); - assert(!(5_i16 <= 4_i16), '!(5 <= 8)'); - assert_gt(5_i16, 2_i16, '5 > 2'); - assert_ge(5_i16, 2_i16, '5 >= 2'); - assert(!(3_i16 > 3_i16), '!(3 > 3)'); - assert_ge(3_i16, 3_i16, '3 >= 3'); -} - -#[test] -#[should_panic(expected: ('i16_sub Underflow',))] -fn test_i16_sub_underflow_1() { - -0x8000_i16 - 1_i16; -} - -#[test] -#[should_panic(expected: ('i16_sub Underflow',))] -fn test_i16_sub_underflow_2() { - -0x8000_i16 - 3_i16; -} - -#[test] -#[should_panic(expected: ('i16_sub Underflow',))] -fn test_i16_sub_underflow_3() { - -0x7fff_i16 - 3_i16; -} - -#[test] -#[should_panic(expected: ('i16_sub Underflow',))] -fn test_i16_sub_underflow_4() { - -0x3200_i16 - 0x7d00_i16; -} - -#[test] -#[should_panic(expected: ('i16_sub Overflow',))] -fn test_i16_sub_overflow() { - 0x3200_i16 - -0x7d00_i16; -} - -#[test] -#[should_panic(expected: ('i16_add Overflow',))] -fn test_i16_add_overflow_1() { - 0x4000_i16 + 0x4000_i16; -} - -#[test] -#[should_panic(expected: ('i16_add Overflow',))] -fn test_i16_add_overflow_2() { - 0x6400_i16 + 0x1e00_i16; -} - -#[test] -#[should_panic(expected: ('i16_add Underflow',))] -fn test_i16_add_underflow() { - -0x6400_i16 + -0x1e00_i16; -} - -#[test] -#[should_panic] -fn test_i16_mul_overflow_1() { - 0x1000_i16 * 0x1000_i16; -} - -#[test] -#[should_panic] -fn test_i16_mul_overflow_2() { - 0x1100_i16 * 0x1000_i16; -} - -#[test] -#[should_panic] -fn test_i16_mul_overflow_3() { - 2_i16 * 0x4000_i16; -} - -#[test] -fn test_i32_operators() { - assert_eq(@1_i32, @1_i32, '1 == 1'); - assert_ne(@1_i32, @2_i32, '1 != 2'); - assert_eq(@0x7fffffff_felt252.try_into().unwrap(), @0x7fffffff_i32, '0x7fffffff is not i32'); - let v: Option = 0x80000000_felt252.try_into(); - assert(v.is_none(), '0x80000000 is i32'); - assert_eq(@(-0x80000000_felt252).try_into().unwrap(), @-0x80000000_i32, '-0x8000 is not i32'); - let v: Option = (-0x80000001_felt252).try_into(); - assert(v.is_none(), '-0x80000001 is i32'); - assert_eq(@(1_i32 + 3_i32), @4_i32, '1 + 3 == 4'); - assert_eq(@(3_i32 + 6_i32), @9_i32, '3 + 6 == 9'); - assert_eq(@(3_i32 - 1_i32), @2_i32, '3 - 1 == 2'); - assert_eq(@(231_i32 - 131_i32), @100_i32, '231-131=100'); - assert_eq(@(-1_i32 + -3_i32), @-4_i32, '-1 + -3 == -4'); - assert_eq(@(-3_i32 + -6_i32), @-9_i32, '-3 + -6 == -9'); - assert_eq(@(-3_i32 - -1_i32), @-2_i32, '-3 - -1 == -2'); - assert_eq(@(-231_i32 - -131_i32), @-100_i32, '-231--131=-100'); - assert_eq(@(1_i32 * 3_i32), @3_i32, '1 * 3 == 3'); - assert_eq(@(2_i32 * 4_i32), @8_i32, '2 * 4 == 8'); - assert_eq(@(-1_i32 * 3_i32), @-3_i32, '-1 * 3 == 3'); - assert_eq(@(-2_i32 * 4_i32), @-8_i32, '-2 * 4 == 8'); - assert_eq(@(1_i32 * -3_i32), @-3_i32, '1 * -3 == -3'); - assert_eq(@(2_i32 * -4_i32), @-8_i32, '2 * -4 == -8'); - assert_eq(@(-1_i32 * -3_i32), @3_i32, '-1 * -3 == 3'); - assert_eq(@(-2_i32 * -4_i32), @8_i32, '-2 * -4 == 8'); - assert_lt(1_i32, 4_i32, '1 < 4'); - assert_le(1_i32, 4_i32, '1 <= 4'); - assert(!(4_i32 < 4_i32), '!(4 < 4)'); - assert_le(5_i32, 5_i32, '5 <= 5'); - assert(!(5_i32 <= 4_i32), '!(5 <= 8)'); - assert_gt(5_i32, 2_i32, '5 > 2'); - assert_ge(5_i32, 2_i32, '5 >= 2'); - assert(!(3_i32 > 3_i32), '!(3 > 3)'); - assert_ge(3_i32, 3_i32, '3 >= 3'); -} - -#[test] -#[should_panic(expected: ('i32_sub Underflow',))] -fn test_i32_sub_underflow_1() { - -0x80000000_i32 - 1_i32; -} - -#[test] -#[should_panic(expected: ('i32_sub Underflow',))] -fn test_i32_sub_underflow_2() { - -0x80000000_i32 - 3_i32; -} - -#[test] -#[should_panic(expected: ('i32_sub Underflow',))] -fn test_i32_sub_underflow_3() { - -0x7fffffff_i32 - 3_i32; -} - -#[test] -#[should_panic(expected: ('i32_sub Underflow',))] -fn test_i32_sub_underflow_4() { - -0x32000000_i32 - 0x7d000000_i32; -} - -#[test] -#[should_panic(expected: ('i32_sub Overflow',))] -fn test_i32_sub_overflow() { - 0x32000000_i32 - -0x7d000000_i32; -} - -#[test] -#[should_panic(expected: ('i32_add Overflow',))] -fn test_i32_add_overflow_1() { - 0x40000000_i32 + 0x40000000_i32; -} - -#[test] -#[should_panic(expected: ('i32_add Overflow',))] -fn test_i32_add_overflow_2() { - 0x64000000_i32 + 0x1e000000_i32; -} - -#[test] -#[should_panic(expected: ('i32_add Underflow',))] -fn test_i32_add_underflow() { - -0x64000000_i32 + -0x1e000000_i32; -} - -#[test] -#[should_panic] -fn test_i32_mul_overflow_1() { - 0x10000000_i32 * 0x10000000_i32; -} - -#[test] -#[should_panic] -fn test_i32_mul_overflow_2() { - 0x11000000_i32 * 0x10000000_i32; -} - -#[test] -#[should_panic] -fn test_i32_mul_overflow_3() { - 2_i32 * 0x40000000_i32; -} - -#[test] -fn test_i64_operators() { - assert_eq(@1_i64, @1_i64, '1 == 1'); - assert_ne(@1_i64, @2_i64, '1 != 2'); - assert_eq( - @0x7fffffffffffffff_felt252.try_into().unwrap(), - @0x7fffffffffffffff_i64, - '0x7fffffffffffffff is not i64' - ); - let v: Option = 0x8000000000000000_felt252.try_into(); - assert(v.is_none(), '0x8000000000000000 is i64'); - assert_eq( - @(-0x8000000000000000_felt252).try_into().unwrap(), - @-0x8000000000000000_i64, - '-0x8000000000000000 is not i64' - ); - let v: Option = (-0x8000000000000001_felt252).try_into(); - assert(v.is_none(), '-0x8000000000000001 is i64'); - assert_eq(@(1_i64 + 3_i64), @4_i64, '1 + 3 == 4'); - assert_eq(@(3_i64 + 6_i64), @9_i64, '3 + 6 == 9'); - assert_eq(@(3_i64 - 1_i64), @2_i64, '3 - 1 == 2'); - assert_eq(@(231_i64 - 131_i64), @100_i64, '231-131=100'); - assert_eq(@(-1_i64 + -3_i64), @-4_i64, '-1 + -3 == -4'); - assert_eq(@(-3_i64 + -6_i64), @-9_i64, '-3 + -6 == -9'); - assert_eq(@(-3_i64 - -1_i64), @-2_i64, '-3 - -1 == -2'); - assert_eq(@(-231_i64 - -131_i64), @-100_i64, '-231--131=-100'); - assert_eq(@(1_i64 * 3_i64), @3_i64, '1 * 3 == 3'); - assert_eq(@(2_i64 * 4_i64), @8_i64, '2 * 4 == 8'); - assert_eq(@(-1_i64 * 3_i64), @-3_i64, '-1 * 3 == 3'); - assert_eq(@(-2_i64 * 4_i64), @-8_i64, '-2 * 4 == 8'); - assert_eq(@(1_i64 * -3_i64), @-3_i64, '1 * -3 == -3'); - assert_eq(@(2_i64 * -4_i64), @-8_i64, '2 * -4 == -8'); - assert_eq(@(-1_i64 * -3_i64), @3_i64, '-1 * -3 == 3'); - assert_eq(@(-2_i64 * -4_i64), @8_i64, '-2 * -4 == 8'); - assert_lt(1_i64, 4_i64, '1 < 4'); - assert_le(1_i64, 4_i64, '1 <= 4'); - assert(!(4_i64 < 4_i64), '!(4 < 4)'); - assert_le(5_i64, 5_i64, '5 <= 5'); - assert(!(5_i64 <= 4_i64), '!(5 <= 8)'); - assert_gt(5_i64, 2_i64, '5 > 2'); - assert_ge(5_i64, 2_i64, '5 >= 2'); - assert(!(3_i64 > 3_i64), '!(3 > 3)'); - assert_ge(3_i64, 3_i64, '3 >= 3'); -} - -#[test] -#[should_panic(expected: ('i64_sub Underflow',))] -fn test_i64_sub_underflow_1() { - -0x8000000000000000_i64 - 1_i64; -} - -#[test] -#[should_panic(expected: ('i64_sub Underflow',))] -fn test_i64_sub_underflow_2() { - -0x8000000000000000_i64 - 3_i64; -} - -#[test] -#[should_panic(expected: ('i64_sub Underflow',))] -fn test_i64_sub_underflow_3() { - -0x7fffffffffffffff_i64 - 3_i64; -} - -#[test] -#[should_panic(expected: ('i64_sub Underflow',))] -fn test_i64_sub_underflow_4() { - -0x3200000000000000_i64 - 0x7d00000000000000_i64; -} - -#[test] -#[should_panic(expected: ('i64_sub Overflow',))] -fn test_i64_sub_overflow() { - 0x3200000000000000_i64 - -0x7d00000000000000_i64; -} - -#[test] -#[should_panic(expected: ('i64_add Overflow',))] -fn test_i64_add_overflow_1() { - 0x4000000000000000_i64 + 0x4000000000000000_i64; -} - -#[test] -#[should_panic(expected: ('i64_add Overflow',))] -fn test_i64_add_overflow_2() { - 0x6400000000000000_i64 + 0x1e00000000000000_i64; -} - -#[test] -#[should_panic(expected: ('i64_add Underflow',))] -fn test_i64_add_underflow() { - -0x6400000000000000_i64 + -0x1e00000000000000_i64; -} - -#[test] -#[should_panic] -fn test_i64_mul_overflow_1() { - 0x1000000000000000_i64 * 0x1000000000000000_i64; -} - -#[test] -#[should_panic] -fn test_i64_mul_overflow_2() { - 0x1100000000000000_i64 * 0x1000000000000000_i64; -} - -#[test] -#[should_panic] -fn test_i64_mul_overflow_3() { - 2_i64 * 0x4000000000000000_i64; -} - -#[test] -fn test_i128_operators() { - assert_eq(@1_i128, @1_i128, '1 == 1'); - assert_ne(@1_i128, @2_i128, '1 != 2'); - assert_eq( - @0x7fffffffffffffffffffffffffffffff_felt252.try_into().unwrap(), - @0x7fffffffffffffffffffffffffffffff_i128, - '0x7f..f is not i128' - ); - let v: Option = 0x80000000000000000000000000000000_felt252.try_into(); - assert(v.is_none(), '0x80..0 is i128'); - assert_eq( - @(-0x80000000000000000000000000000000_felt252).try_into().unwrap(), - @-0x80000000000000000000000000000000_i128, - '-0x80..0 is not i128' - ); - let v: Option = (-0x80000000000000000000000000000001_felt252).try_into(); - assert(v.is_none(), '-0x80..01 is i128'); - assert_eq(@(1_i128 + 3_i128), @4_i128, '1 + 3 == 4'); - assert_eq(@(3_i128 + 6_i128), @9_i128, '3 + 6 == 9'); - assert_eq(@(3_i128 - 1_i128), @2_i128, '3 - 1 == 2'); - assert_eq(@(231_i128 - 131_i128), @100_i128, '231-131=100'); - assert_eq(@(-1_i128 + -3_i128), @-4_i128, '-1 + -3 == -4'); - assert_eq(@(-3_i128 + -6_i128), @-9_i128, '-3 + -6 == -9'); - assert_eq(@(-3_i128 - -1_i128), @-2_i128, '-3 - -1 == -2'); - assert_eq(@(-231_i128 - -131_i128), @-100_i128, '-231--131=-100'); - assert_eq(@(1_i128 * 3_i128), @3_i128, '1 * 3 == 3'); - assert_eq(@(7_i128 * 0_i128), @0_i128, '7 * 0 == 0'); - assert_eq(@(2_i128 * 4_i128), @8_i128, '2 * 4 == 8'); - assert_eq(@(-1_i128 * 3_i128), @-3_i128, '-1 * 3 == -3'); - assert_eq(@(-2_i128 * 4_i128), @-8_i128, '-2 * 4 == -8'); - assert_eq(@(1_i128 * -3_i128), @-3_i128, '1 * -3 == -3'); - assert_eq(@(2_i128 * -4_i128), @-8_i128, '2 * -4 == -8'); - assert_eq(@(-1_i128 * -3_i128), @3_i128, '-1 * -3 == 3'); - assert_eq(@(-2_i128 * -4_i128), @8_i128, '-2 * -4 == 8'); - assert_eq( - @(0x800000000000000_i128 * -0x100000000000000000_i128), - @-0x80000000000000000000000000000000_i128, - 'failed MIN_I128 as mul result' - ); - assert_lt(1_i128, 4_i128, '1 < 4'); - assert_le(1_i128, 4_i128, '1 <= 4'); - assert(!(4_i128 < 4_i128), '!(4 < 4)'); - assert_le(5_i128, 5_i128, '5 <= 5'); - assert(!(5_i128 <= 4_i128), '!(5 <= 8)'); - assert_gt(5_i128, 2_i128, '5 > 2'); - assert_ge(5_i128, 2_i128, '5 >= 2'); - assert(!(3_i128 > 3_i128), '!(3 > 3)'); - assert_ge(3_i128, 3_i128, '3 >= 3'); -} - -#[test] -#[should_panic] -fn test_i128_sub_underflow_1() { - -0x80000000000000000000000000000000_i128 - 1_i128; -} - -#[test] -#[should_panic(expected: ('i128_sub Underflow',))] -fn test_i128_sub_underflow_2() { - -0x80000000000000000000000000000000_i128 - 3_i128; -} - -#[test] -#[should_panic(expected: ('i128_sub Underflow',))] -fn test_i128_sub_underflow_3() { - -0x7fffffffffffffffffffffffffffffff_i128 - 3_i128; -} - -#[test] -#[should_panic(expected: ('i128_sub Underflow',))] -fn test_i128_sub_underflow_4() { - -0x32000000000000000000000000000000_i128 - 0x7d000000000000000000000000000000_i128; -} - -#[test] -#[should_panic(expected: ('i128_sub Overflow',))] -fn test_i128_sub_overflow() { - 0x32000000000000000000000000000000_i128 - -0x7d000000000000000000000000000000_i128; -} - -#[test] -#[should_panic(expected: ('i128_add Overflow',))] -fn test_i128_add_overflow_1() { - 0x40000000000000000000000000000000_i128 + 0x40000000000000000000000000000000_i128; -} - -#[test] -#[should_panic(expected: ('i128_add Overflow',))] -fn test_i128_add_overflow_2() { - 0x64000000000000000000000000000000_i128 + 0x1e000000000000000000000000000000_i128; -} - -#[test] -#[should_panic(expected: ('i128_add Underflow',))] -fn test_i128_add_underflow() { - -0x64000000000000000000000000000000_i128 + -0x1e000000000000000000000000000000_i128; -} - -#[test] -#[should_panic] -fn test_i128_mul_overflow_1() { - 0x10000000000000000000000000000000_i128 * 0x10000000000000000000000000000000_i128; -} - -#[test] -#[should_panic] -fn test_i128_mul_overflow_2() { - 0x11000000000000000000000000000000_i128 * 0x10000000000000000000000000000000_i128; -} - -#[test] -#[should_panic] -fn test_i128_mul_overflow_3() { - 2_i128 * 0x40000000000000000000000000000000_i128; -} - -#[test] -fn test_signed_int_diff() { - assert_eq(@integer::i8_diff(3, 3).unwrap(), @0, 'i8: 3 - 3 == 0'); - assert_eq(@integer::i8_diff(4, 3).unwrap(), @1, 'i8: 4 - 3 == 1'); - assert_eq(@integer::i8_diff(3, 5).unwrap_err(), @~(2 - 1), 'i8: 3 - 5 == -2'); - assert_eq(@integer::i16_diff(3, 3).unwrap(), @0, 'i16: 3 - 3 == 0'); - assert_eq(@integer::i16_diff(4, 3).unwrap(), @1, 'i16: 4 - 3 == 1'); - assert_eq(@integer::i16_diff(3, 5).unwrap_err(), @~(2 - 1), 'i16: 3 - 5 == -2'); - assert_eq(@integer::i32_diff(3, 3).unwrap(), @0, 'i32: 3 - 3 == 0'); - assert_eq(@integer::i32_diff(4, 3).unwrap(), @1, 'i32: 4 - 3 == 1'); - assert_eq(@integer::i32_diff(3, 5).unwrap_err(), @~(2 - 1), 'i32: 3 - 5 == -2'); - assert_eq(@integer::i64_diff(3, 3).unwrap(), @0, 'i64: 3 - 3 == 0'); - assert_eq(@integer::i64_diff(4, 3).unwrap(), @1, 'i64: 4 - 3 == 1'); - assert_eq(@integer::i64_diff(3, 5).unwrap_err(), @~(2 - 1), 'i64: 3 - 5 == -2'); - assert_eq(@integer::i128_diff(3, 3).unwrap(), @0, 'i128: 3 - 3 == 0'); - assert_eq(@integer::i128_diff(4, 3).unwrap(), @1, 'i128: 4 - 3 == 1'); - assert_eq(@integer::i128_diff(3, 5).unwrap_err(), @~(2 - 1), 'i128: 3 - 5 == -2'); -} - -mod special_casts { - extern type BoundedInt; - extern fn downcast(index: T) -> Option implicits(RangeCheck) nopanic; - extern fn upcast(index: T) -> S nopanic; - - impl DropBoundedInt120_180 of Drop>; - const U128_UPPER: felt252 = 0x100000000000000000000000000000000; - type BoundedIntU128Upper = - BoundedInt<0x100000000000000000000000000000000, 0x100000000000000000000000000000000>; - const U128_MAX: felt252 = 0xffffffffffffffffffffffffffffffff; - type BoundedIntU128Max = - BoundedInt<0xffffffffffffffffffffffffffffffff, 0xffffffffffffffffffffffffffffffff>; - - /// Is `value` the equivalent value of `expected` in `T` type. - fn is_some_of(value: Option, expected: felt252) -> bool { - match value { - Option::Some(v) => upcast(v) == expected, - Option::None => false, - } - } - - /// Is `value` the equivalent value (as `felt252`) of `expected` in `T` type. - fn felt252_downcast_valid(value: felt252) -> bool { - is_some_of(downcast::(value), value) - } - - /// Is `value` the equivalent value (as `felt252`) of `expected` in `T` type. - fn downcast_invalid(value: T) -> bool { - match downcast::(value) { - Option::Some(v) => { - // Just as a drop for `v`. - upcast::<_, felt252>(v); - false - }, - Option::None => true, - } - } - - #[test] - fn test_felt252_downcasts() { - assert!(downcast_invalid::>(1)); - assert!(felt252_downcast_valid::>(0)); - assert!(downcast_invalid::>(-1)); - assert!(downcast_invalid::>(-2)); - assert!(felt252_downcast_valid::>(-1)); - assert!(downcast_invalid::>(0)); - assert!(downcast_invalid::>(119)); - assert!(felt252_downcast_valid::>(120)); - assert!(felt252_downcast_valid::>(180)); - assert!(downcast_invalid::>(181)); - assert!(downcast_invalid::(U128_MAX - 1)); - assert!(felt252_downcast_valid::(U128_MAX)); - assert!(downcast_invalid::(U128_MAX + 1)); - assert!(downcast_invalid::(U128_UPPER - 1)); - assert!(felt252_downcast_valid::(U128_UPPER)); - assert!(downcast_invalid::(U128_UPPER + 1)); - } - - // Full prime range, but where the max element is 0. - type OneMinusPToZero = - BoundedInt<-0x800000000000011000000000000000000000000000000000000000000000000, 0>; - - type OneMinusPOnly = - BoundedInt< - -0x800000000000011000000000000000000000000000000000000000000000000, - -0x800000000000011000000000000000000000000000000000000000000000000 - >; - - #[test] - fn test_bounded_int_casts() { - let minus_1 = downcast::>(-1).unwrap(); - assert!(downcast::(upcast(minus_1)).is_none()); - let zero = downcast::>(0).unwrap(); - assert!(downcast::(upcast(zero)) == Option::Some(0)); - let one_minus_p = downcast::(1).unwrap(); - assert!(downcast::(upcast(one_minus_p)).is_none()); - let v119 = downcast::>(119).unwrap(); - assert!(downcast::, BoundedInt<120, 180>>(upcast(v119)).is_none()); - let v120 = downcast::>(120).unwrap(); - assert!( - is_some_of(downcast::, BoundedInt<120, 180>>(upcast(v120)), 120) - ); - let v180 = downcast::>(180).unwrap(); - assert!( - is_some_of(downcast::, BoundedInt<120, 180>>(upcast(v180)), 180) - ); - let v181 = downcast::>(181).unwrap(); - assert!(downcast::, BoundedInt<120, 180>>(upcast(v181)).is_none()); - } -} diff --git a/docs/compilation_walkthrough.md b/docs/compilation_walkthrough.md index 46a60705da..a49f324c32 100644 --- a/docs/compilation_walkthrough.md +++ b/docs/compilation_walkthrough.md @@ -1,20 +1,22 @@ -# Compilation Walkthrough -This section describes the entire process Cairo Native goes through to -compile a Cairo program to either a shared library (and how to use it) or a -MLIR module for use in the JIT engine. +# Compilation walkthrough + +This section describes the entire process Cairo Native goes through to compile a +Cairo program to either a shared library (and how to use it) or a MLIR module +for use in the JIT engine. ## General flow + If you check `lib.rs` you will see the high level modules of the project. -The compiler module is what glues together everything. -You should read its module level documentation. -But the basic flow is like this: -- We take a sierra `Program` and iterate over its functions. -- On each function, we create a MLIR region and a block for each statement - (a.k.a library function call), taking into account possible branches. -- On each statement we call the library function implementation, which - appends MLIR code to the given block, and with helper methods, it handles - possible branches and input/output variables. +The compiler module is what glues together everything. You should read its +module level documentation, but the basic flow goes like this: + +1. We take a Sierra `Program` and iterate over its functions. +2. For each function, we create a MLIR region which has a block for each + statement (a.k.a libfunc invocation), taking into account possible branches. +3. For each statement we generate the libfunc implementation in their respective + block. The block is terminated by the branch operation generated by the + libfunc helper, which has enough information to handle branching properly. ```mermaid stateDiagram-v2 @@ -57,8 +59,9 @@ stateDiagram-v2 ``` ## Loading a Cairo Program -The first step is to get the sierra code from the given cairo program, this -is done using the relevant methods from the `cairo_lang_compiler` crate. + +The first step is to get the Sierra code from the given cairo program, this is +done using the relevant methods from the `cairo_lang_compiler` crate. This gives us a `cairo_lang_sierra::program::Program` which has the following structure: @@ -72,111 +75,122 @@ pub struct Program { } ``` -The compilation process consists in parsing these fields to produce the -relevant MLIR IR code. +The compilation process uses these fields through the `ProgramRegistry` to +generate the relevant MLIR IR code. -To do all this we will need a MLIR Context and a module created with that -context, the module describes a compilation unit, in this case, the cairo +To do all this we will need a MLIR context and a module created with said +context, the module describes a compilation unit, in this case, the Cairo program. ## Initialization -In Cairo Native we provide a API around initializing the context, namely +In Cairo Native we provide an API around context initialization, namely `NativeContext` which does the following when -[created](https://github.com/lambdaclass/cairo_native/blob/ca6549a68c1b4266a7f9ea41dc196bf4433a2ee8/src/context.rs#L52-L53): +[created](https://github.com/lambdaclass/cairo_native/blob/main/src/context.rs#L56-L59): -- Create the context -- Register all relevant MLIR dialects into the context -- Load the dialects -- Register all passes into the context -- Register all translations to LLVM IR into the context. +1. Create the context. +2. Register all relevant MLIR dialects into the context. +3. Load the dialects. +4. Register all passes into the context. +5. Register all translations (ex. to LLVM IR) into the context. ## Compiling a Sierra Program to MLIR The `NativeContext` has a method called -[compile](https://github.com/lambdaclass/cairo_native/blob/ca6549a68c1b4266a7f9ea41dc196bf4433a2ee8/src/context.rs#L62-L63), -which does the heavy lifting and returns a `NativeModule`. -This module contains the generated MLIR IR code. +[compile](https://github.com/lambdaclass/cairo_native/blob/main/src/context.rs#L70-L233), +which does the heavy lifting and returns a `NativeModule`. That module contains +the generated MLIR IR code. The compile method does the following: -- Create a Module -- Create the Metadata storage (check the relevant section for more information). -- Check if the Sierra program has a gas builtin in it, if it has it will - insert the gas metadata into the storage. -- Create the Sierra program registry, which allows type and function lookups. -- Call a internal `compile` method. -This internal `compile` method then loops on the program function -declarations calling the `compile_func` method on each of them. +1. Create the MLIR module. +2. Create the metadata storage (check relevant sections for more info). +3. Check if the Sierra program uses the gas builtin. If it does, it will insert + the gas metadata into the metadata storage. +4. Create the Sierra program registry, which allows for easy type, libfunc and + function lookups. +5. Call the internal `compile` method. -### Compiling a function (`compile_func`) +This internal `compile` method then loops on the program function declarations, +calling the `compile_func` method on each of them. -This method generates the structure of the function in MLIR, meaning it will -create the region the body of the function will live on, and then a block -for each statement, each with it’s relevant arguments and return values. It -will also check each statement whether it is branching, and store the -predecessors of each block, to handle jumps. +### Compiling a function (`compile_func`) -While handling each statement on the function, it will build the types it -finds from the arguments and return values as it encounters them, this is -done using the trait `TypeBuilder`. +This method first generates the structure of the function in MLIR, meaning it +will create the region the body of the function will live on, and then a block +for each statement, each with it’s relevant arguments and return values. It will +also check each statement whether it is branching, and store the predecessors of +each block. The predecessors are used to create the auxiliary landing blocks. -After having the function structure created, we proceed to creating the -initial state, which is a Hash map holding the local variables we currently -have, the parameters. +After finishing the function structure, we need to generate the initial state, +which is a hash map holding the data we currently have, that is, the function +arguments. -Using this initial state, it builds the entry block, which is the first -block the function enters when it’s called, it has the function arguments -as parameters. +Starting from this initial state, the compiler walks the entire tree of +statements in a depth-first manner, generating the implementation for all the +invocations in order. While this happens, the state is updated accordingly (the +invocation arguments are removed while the results are inserted). Then it loops on the statements of the function, on each statement it does the following: -- Check if there is a gas metadata, and if the statement has a gas cost, - insert the gas cost metadata that lives on only during this statement. -- Get the block and possible landing block of this statement. -- If there is a landing block, create it. A landing block is the target - block of a previous jump that simply forwards to the current block. +- Check if there the gas metadata is present and whether the current statement + has a cost, then insert the `GasCost` metadata if it exists. This metadata + will be removed before the next statement is processed. +- Get the block and potential landing block for this statement. +- Build the statement. This step may require extra blocks, which will be + inserted immediately after the first one. + +While handling each statement on the function, the libfunc code generators may +build the types they need (ex. arguments and return values) using the +`TypeBuilder` trait. ## Metadata Storage -This storage is shared everywhere in the compilation process and allows to -easily share data to the relevant places, for example the Gas Metadata -allows getting the gas cost for a given statement, or the enum snapshot -metadata to get the relevant variants in the libfunc builder. + +This storage is passed around within the compilation process and allows to +easily share data that would otherwise be difficult to implement if the need was +not known beforehand. Some examples are the gas metadata, which provides the gas +cost for any given statement, or the drop and dup overrides, that contain the +specializations for the `drop` and `dup` libfuncs for respectively. # Compiling to native code -We part from the point where the program has been compiled into MLIR IR, -and we hold the MLIR Context and Module. +At this point, the Sierra program has already been transpiled into MLIR and is +currently stored within the module. -From this point, we convert all the dialects within this IR into the LLVM -MLIR Dialect, which is a needed precondition to transform the MLIR IR into -LLVM IR. This is done through passes which are the basis of how LLVM works. +The MLIR code is exactly as we've generated it, which means it'll use any +dialect we've found to be useful for transpiling the code. This isn't ideal as +we need everything to be in the LLVM dialect, which is what can be translated +into LLVM IR. This conversion is performed through passes which are the building +blocks of MLIR and LLVM optimizations. -Given a MLIR Module with only the LLVM dialect, we can translate it, -currently the LLVM MLIR API for this is only available in C++, so we had -to make our temporary C API wrapper (which we contributed to upstream LLVM. -After that we also need to use the `llvm-sys` crate which provides the C -API bindings in Rust. +Since we now have the entire program transpiled using only the LLVM dialect, we +can proceed to translate it to LLVM IR. The APIs to do that weren't available +for C when we started, so we contributed them to upstream LLVM. This also meant +we've had to use the `llvm-sys` crate directly, which contains the raw LLVM C +bindings. -The required method is `mlirTranslateModuleToLLVMIR` which takes a MLIR -Module and a LLVM Context (not a MLIR one!). The LLVM Context will be used -to create a LLVM Module, which we can then compile to machine code. +The function we need to translate MLIR to LLVM IR is called +`mlirTranslateModuleToLLVMIR`. It takes the MLIR module and an LLVM context, +which will be the one associated with the LLVM IR module. The resulting module +can then be fed to LLVM to be compiled into machine code. -The process is a bit verbose but interesting, LLVM itself is a target -independent code generator, but to compile down we need an actual target, -to do so we initialize the required target and utilities (in this case we -initialize all targets the current compiled LLVM supports): +The process is a bit verbose but interesting nontheless. LLVM itself is a target +independent code generator, but to compile the module down to machine code we +need an actual target. The targets and all their dependencies can be initialized +by calling the following functions: ```rust,ignore LLVM_InitializeAllTargets(); @@ -186,43 +200,45 @@ LLVM_InitializeAllAsmPrinters(); LLVM_InitializeAllAsmParsers(); ``` -After that we create a LLVM context, and pass it along the module to the -`mlirTranslateModuleToLLVMIR` method: +Once everything's been initialized, we can create the LLVM context and use it +to translate the MLIR module into LLVM IR: ```rust ,ignore let llvm_module = mlirTranslateModuleToLLVMIR(mlir_module_op, llvm_context); ``` -Then we need to create the target machine, which needs a target triple, the -CPU name and CPU features. After creating the target machine, we can emit -the object file either to a memory buffer or a file. +The target we're compiling for is defined by the target triple, which contains +the CPU architecture, the vendor and the operating system. We can also specify +which features our CPU supports to allow LLVM to generate more optimized machine +code. ```rust,ignore let machine = LLVMCreateTargetMachine( - target, - target_triple.cast(), - target_cpu.cast(), - target_cpu_features.cast(), - LLVMCodeGenOptLevel::LLVMCodeGenLevelNone, // opt level - LLVMRelocMode::LLVMRelocDynamicNoPic, - LLVMCodeModel::LLVMCodeModelDefault, + target, + target_triple.cast(), + target_cpu.cast(), + target_cpu_features.cast(), + LLVMCodeGenOptLevel::LLVMCodeGenLevelNone, + LLVMRelocMode::LLVMRelocDynamicNoPic, + LLVMCodeModel::LLVMCodeModelDefault, ); let mut out_buf: MaybeUninit = MaybeUninit::uninit(); - LLVMTargetMachineEmitToMemoryBuffer( - machine, - llvm_module, - LLVMCodeGenFileType::LLVMObjectFile, - error_buffer, - out_buf.as_mut_ptr(), + machine, + llvm_module, + LLVMCodeGenFileType::LLVMObjectFile, + error_buffer, + out_buf.as_mut_ptr(), ); + +// Error checking has been omitted. +let out_buf = out_buf.assume_init(); ``` -After emitting the object file, we need to pass it to a linker to get our -shared library. This is currently done by executing `ld`, with the proper -flags to create a shared library on each platform, as a process using a -temporary file, because it can’t be piped. +After emitting the object file, we need to link it into a shared library that we +can load. This is currently done by running `ld` with the correct flags and a +temporary file, as `ld`'s output cannot be piped. ```mermaid graph TD @@ -236,31 +252,74 @@ graph TD E --> |Link the object file| G[Shared Library] ``` -## Loading the native library and using it -To load the library, we use the crate `libloading`, passing it the path to -our shared library. +## Loading and using the native library + +Shared libraries can be loaded at runtime by using the OS-provided facilities, +which on Linux and Mac OS are the `dlopen`, `dlclose` and `dlsym` functions. We +could use those, but the `libloading` crate wraps them in an easy to use and +safer manner. -Then we initialize the `AotNativeExecutor` with the loaded library and the -program registry. +Once the library is loaded we can initialize an `AotNativeExecutor` instance +using the shared library handle and the program registry. Internally, the +following happens: -This initialization internally does the following: -- Constructs the symbol of the function to be called, which is always the - function name but wrapped with a prefix to instead target the C API - wrapper, it looks like the following: +1. The symbol of the function is generated so that we can reference it: ```rust,ignore +// The `generate_function_name` function handles special cases like when the +// debug info is not available or when we're using the `AotContractExecutor`. +let function_name = generate_function_name(function_id, false); let function_name = format!("_mlir_ciface_{function_name}"); ``` -- Using the registry we get the function signature, although `libloading` - allows us to have a function signature at compile time to make sure we - call it properly, but we need to ignore this as we want to call any - function given the library and the registry. +2. We can then obtain the pointer to the symbol, which is a function pointer. If + we knew at compile-time the function's signature we could cast that pointer + into an `extern "C" fn()`, but since it's dynamic we cannot rely on that. + Instead, we use the trampoline. + + + +3. To invoke the trampoline we need to process the function arguments (including + the builtins) so that they are in the layout expected by the architecture's + C function call convention. +3. After we've generated the function argument buffer with that specific + platform-dependent layout, we can invoke the trampoline. It'll move the data + around and call into the MLIR-generated machine code. Once it returns, the + trampoline will read the results and store them in a place that is accessible + by Rust. + +4. On Rust's side, we can parse the result (either in registers or in a + pre-allocated memory space) and convert it back into `Value`s so that they + can be returned. + ```mermaid graph TD A[Load library with libloading] --> B[Get the function pointer] @@ -275,12 +334,13 @@ graph TD ## Addendum ### About canonicalization in MLIR: -MLIR has a single canonicalization pass, which iteratively applies the -canonicalization patterns of all loaded dialects in a greedy way. -Canonicalization is best-effort and not guaranteed to bring the entire IR in -a canonical form. It applies patterns until either fix point is reached or -the maximum number of iterations/rewrites (as specified via pass options) is -exhausted. This is for efficiency reasons and to ensure that faulty patterns -cannot cause infinite looping. + +MLIR's canonicalization pass iteratively applies its patterns, which span +through all the dialects, in a greedy way. Canonicalization works on a +best-effort basis and is not guaranteed to bring the entire IR to a canonical +form. It applies patterns until either a local minimum is reached or the +iteration limit (as specified via pass options) is reached. This decision exists +for efficiency reasons and to ensure that faulty (unstable) patterns cannot be +the cause of infinite loops, thus deadlocking the entire thread. Good read about this: [https://sunfishcode.github.io/blog/2018/10/22/Canonicalization.html](https://sunfishcode.github.io/blog/2018/10/22/Canonicalization.html) diff --git a/src/compiler.rs b/src/compiler.rs index 2e998d2419..571900f343 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -461,7 +461,6 @@ fn compile_func( |statement_idx, mut state| { if let Some(gas_metadata) = metadata.get::() { let gas_cost = gas_metadata.get_gas_costs_for_statement(statement_idx); - metadata.remove::(); metadata.insert(GasCost(gas_cost)); } @@ -500,7 +499,7 @@ fn compile_func( )); } - Ok(match &statements[statement_idx.0] { + let statement_compilation_result = match &statements[statement_idx.0] { Statement::Invocation(invocation) => { tracing::trace!( "Implementing the invocation statement at {statement_idx}: {}.", @@ -668,7 +667,7 @@ fn compile_func( } } - StatementCompileResult::Processed( + StatementCompilationResult::Processed( invocation .branches .iter() @@ -726,7 +725,7 @@ fn compile_func( // within a tail-recursive function before the recursive call has // been generated. Since we don't have the return target block at // this point we need to defer this return statement's generation. - return Ok(StatementCompileResult::Deferred); + return Ok(StatementCompilationResult::Deferred); } Some((depth_counter, recursion_target)) => { let location = Location::name( @@ -893,9 +892,13 @@ fn compile_func( location, )); - StatementCompileResult::Processed(Vec::new()) + StatementCompilationResult::Processed(Vec::new()) } - }) + }; + + metadata.remove::(); + + Ok(statement_compilation_result) }, )?; @@ -1091,7 +1094,7 @@ fn generate_function_structure<'c, 'a>( } } - StatementCompileResult::Processed( + StatementCompilationResult::Processed( invocation .branches .iter() @@ -1154,7 +1157,7 @@ fn generate_function_structure<'c, 'a>( block.add_argument(ty, location); } - StatementCompileResult::Processed(Vec::new()) + StatementCompilationResult::Processed(Vec::new()) } }) }, @@ -1279,7 +1282,7 @@ fn foreach_statement_in_function( statements: &[Statement], entry_point: StatementIdx, initial_state: S, - mut closure: impl FnMut(StatementIdx, S) -> Result>, E>, + mut closure: impl FnMut(StatementIdx, S) -> Result>, E>, ) -> Result<(), E> where S: Clone, @@ -1293,7 +1296,7 @@ where } match closure(statement_idx, state.clone())? { - StatementCompileResult::Processed(branch_states) => { + StatementCompilationResult::Processed(branch_states) => { let branches = match &statements[statement_idx.0] { Statement::Invocation(x) => x.branches.as_slice(), Statement::Return(_) => &[], @@ -1311,7 +1314,7 @@ where .zip(branch_states), ); } - StatementCompileResult::Deferred => { + StatementCompilationResult::Deferred => { tracing::trace!("Statement {statement_idx}'s compilation has been deferred."); visited.remove(&statement_idx); @@ -1463,7 +1466,7 @@ fn generate_entry_point_wrapper<'c>( /// Return type for the closure in [`foreach_statement_in_function`] that determines whether the /// statement was processed successfully or needs to be processed again at the end. #[derive(Clone, Debug)] -enum StatementCompileResult { +enum StatementCompilationResult { /// The statement was processed successfully. Processed(T), /// The statement's processing has to be deferred until the end. diff --git a/src/metadata/gas.rs b/src/metadata/gas.rs index e84b2b77ce..6db4d9c26a 100644 --- a/src/metadata/gas.rs +++ b/src/metadata/gas.rs @@ -16,16 +16,15 @@ use cairo_lang_sierra::{ ids::FunctionId, program::{Program, StatementIdx}, }; -use cairo_lang_sierra_ap_change::{ap_change_info::ApChangeInfo, ApChangeError}; -use cairo_lang_sierra_gas::{gas_info::GasInfo, CostError}; -use cairo_lang_sierra_to_casm::metadata::{ - calc_metadata, calc_metadata_ap_change_only, Metadata as CairoGasMetadata, - MetadataComputationConfig, MetadataError as CairoGasMetadataError, +use cairo_lang_sierra_ap_change::{ + ap_change_info::ApChangeInfo, calc_ap_changes, + compute::calc_ap_changes as linear_calc_ap_changes, ApChangeError, }; - -use crate::{error::Result as NativeResult, native_panic}; - -use std::{collections::BTreeMap, fmt, ops::Deref}; +use cairo_lang_sierra_gas::{ + compute_postcost_info, compute_precost_info, gas_info::GasInfo, CostError, +}; +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; +use std::collections::BTreeMap; /// Holds global gas info. #[derive(Default)] From 6b8f6d04b7d31ec678c4ff9fc52ba9e53b76f03b Mon Sep 17 00:00:00 2001 From: Esteve Soler Arderiu Date: Thu, 12 Dec 2024 11:59:16 -0300 Subject: [PATCH 02/35] More docs. --- docs/debugging.md | 75 +++++---- docs/execution_walkthrough.md | 296 ++++++++++++++++++++++------------ 2 files changed, 235 insertions(+), 136 deletions(-) diff --git a/docs/debugging.md b/docs/debugging.md index 88dc44f23c..6ff4e828ed 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -19,11 +19,13 @@ export NATIVE_DEBUG_DUMP=1 ### Debugging with LLDB To debug with LLDB (or another debugger), we must compile the binary with the `with-debug-utils` feature. + ```bash cargo build --bin cairo-native-run --features with-debug-utils ``` Then, we can add the a debugger breakpoint trap. To add it at a given sierra statement, we can set the following env var: + ```bash export NATIVE_DEBUG_TRAP_AT_STMT=10 ``` @@ -31,6 +33,7 @@ export NATIVE_DEBUG_TRAP_AT_STMT=10 The trap instruction may not end up exactly where the statement is. If we want to manually set the breakpoint (for example, when executing a particular libfunc), then we can use the `DebugUtils` metadata in the code. + ```rust,ignore #[cfg(feature = "with-debug-utils")] { @@ -47,6 +50,7 @@ lldb -- target/debug/cairo-native-run -s programs/recursion.cairo --available-ga ``` Some usefull lldb commands: + - `process launch`: starts the program - `frame select`: shows the current line information - `thread step-in`: makes a source level single step @@ -54,6 +58,7 @@ Some usefull lldb commands: - `disassemble --frame --mixed`: shows assembly instructions mixed with source level code ## Logging + Enable logging to see the compilation process: ```bash @@ -132,6 +137,7 @@ store_temp([0]) -> ([0]); // 27 ## Debugging Contracts Contracts are difficult to debug for various reasons, including: + - They are external to the project. - We don’t have their source code. - They run autogenerated code (the wrapper). @@ -141,6 +147,7 @@ Contracts are difficult to debug for various reasons, including: Some of them have workarounds: ### Obtaining the contract + There are various options for obtaining the contract, which include: - Manually invoking the a Starknet API using `curl` with the contract class. @@ -169,6 +176,7 @@ Both should provide us with the contract, but if we’re manually invoking the A - Convert the ABI from a string of JSON into a JSON object. ### Interpreting the contract + The contract JSON contains the Sierra program in a useless form (in the sense that we cannot understand anything), as well as some information about the entry points and some ABI types. We’ll need the Sierra program (in Sierra @@ -333,6 +341,7 @@ why it’s important to check for those cases and keep following the control flow backwards as required. ### Fixing the bug + Before fixing the bug it’s really important to know: - **Where** it happens (in our compiler, not so much in the contract at this point) @@ -362,7 +371,7 @@ To aid in the debugging process, we developed [sierra-emu](https://github.com/la In addition to this, we developed the `with-trace-dump` feature for Cairo Native, which generates an execution trace that records every statement executed. It has the same shape as the one generated by the Sierra emulator. Supporting transaction execution with Cairo Native trace dump required quite a few hacks, which is why we haven’t merged it to main. This is why we need to use a specific cairo native branch. -By combining both tools, we can hopefully pinpoint exactly which *libfunc* implementation is buggy. +By combining both tools, we can hopefully pinpoint exactly which _libfunc_ implementation is buggy. Before starting, make sure to clone [starknet-replay](https://github.com/lambdaclass/starknet-replay). @@ -370,9 +379,9 @@ Before starting, make sure to clone [starknet-replay](https://github.com/lambdac 1. Checkout starknet-replay `trace-dump` branch. 2. Execute a single transaction with the `use-sierra-emu` feature - ```bash - cargo run --features use-sierra-emu tx - ``` + ```bash + cargo run --features use-sierra-emu tx + ``` 3. Once finished, it will have written the traces of each inner contract inside of `traces/emu`, relative to the current working directory. As a single transaction can invoke multiple contracts (by contract and library calls), this generates a trace file for each contract executed, numbered in ascending order: `trace_0.json`, `trace_1.json`, etc. @@ -381,9 +390,9 @@ As a single transaction can invoke multiple contracts (by contract and library c 1. Checkout starknet-replay `trace-dump` branch. 2. Execute a single transaction with the `with-trace-dump` feature - ```bash - cargo run --features with-trace-dump tx - ``` + ```bash + cargo run --features with-trace-dump tx + ``` 3. Once finished, it will have written the traces of each inner contract inside of `traces/native`, relative to the current working directory. #### Patching Dependencies @@ -402,41 +411,41 @@ sierra-emu = { path = "../sierra-emu" } Once you have generated the traces for both the Sierra emulator and Cairo Native, you can begin debugging. 1. Compare the traces of the same contract with the favorite tool: - ```bash - diff "traces/{emu,native}/trace_0.json" # or - delta "traces/{emu,native}/trace_0.json" --side-by-side - ``` + ```bash + diff "traces/{emu,native}/trace_0.json" # or + delta "traces/{emu,native}/trace_0.json" --side-by-side + ``` 2. Look for the first significant difference between the traces. Not all the differences are significant, for example: - 1. Sometimes the emulator and Cairo Native differ in the Gas builtin. It usually doesn’t affect the outcome of the contract. - 2. The ec_state_init libfunc randomizes an elliptic curve point, which is why they always differ. + 1. Sometimes the emulator and Cairo Native differ in the Gas builtin. It usually doesn’t affect the outcome of the contract. + 2. The ec_state_init libfunc randomizes an elliptic curve point, which is why they always differ. 3. Find the index of the statement executed immediately previous to the first difference. 4. Open `traces/prog_0.sierra` and look for that statement. - 1. If it’s a return, then you are dealing with a control flow bug. These are difficult to debug. - 2. If it’s a libfunc invocation, then that libfunc is probably the one that is buggy. - 3. If it’s a library or contract call, then the bug is probably in another contract, and you should move onto the next trace. + 1. If it’s a return, then you are dealing with a control flow bug. These are difficult to debug. + 2. If it’s a libfunc invocation, then that libfunc is probably the one that is buggy. + 3. If it’s a library or contract call, then the bug is probably in another contract, and you should move onto the next trace. #### Useful Scripts In the `scripts` folder of starknet-replay, you can find useful scripts for debugging. Make sure to execute them in the root directory. Some scripts require `delta` to be installed. - `compare-traces`: Compares every trace and outputs which are different. This can help finding the buggy contract when there are a lot of traces. - ```bash - > ./scripts/compare-traces.sh - difference: ./traces/emu/trace_0.json ./traces/native/trace_0.json - difference: ./traces/emu/trace_1.json ./traces/native/trace_1.json - difference: ./traces/emu/trace_3.json ./traces/native/trace_3.json - missing file: ./traces/native/trace_4.json - ``` + ```bash + > ./scripts/compare-traces.sh + difference: ./traces/emu/trace_0.json ./traces/native/trace_0.json + difference: ./traces/emu/trace_1.json ./traces/native/trace_1.json + difference: ./traces/emu/trace_3.json ./traces/native/trace_3.json + missing file: ./traces/native/trace_4.json + ``` - `diff-trace`: Receives a trace number, and executes `delta` to compare that trace. - ```bash - ./scripts/diff-trace.sh 1 - ``` + ```bash + ./scripts/diff-trace.sh 1 + ``` - `diff-trace-flow`: Like `diff-trace`, but only diffs (with `delta`) the statement indexes. It can be used to visualize the control flow difference. - ```bash - ./scripts/diff-trace-flow.sh 1 - ``` + ```bash + ./scripts/diff-trace-flow.sh 1 + ``` - `string-to-felt`: Converts the given string to a felt. Can be used to search in the code where a specific error message was generated. - ```bash - > ./scripts/string-to-felt.sh "u256_mul Overflow" - 753235365f6d756c204f766572666c6f77 - ``` + ```bash + > ./scripts/string-to-felt.sh "u256_mul Overflow" + 753235365f6d756c204f766572666c6f77 + ``` diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index d5b78d26f9..47ce224998 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -1,104 +1,136 @@ # Execution Walkthrough -Given the following Cairo program: +Let's walk through the execution of the following Cairo program: ```rust,ignore // This is the cairo program. It just adds two numbers together and returns the // result in an enum whose variant is selected using the result's parity. enum Parity { - Even: T, - Odd: T, + Even: T, + Odd: T, } + /// Add `lhs` and `rhs` together and return the result in `Parity::Even` if it's /// even or `Parity::Odd` otherwise. fn run(lhs: u128, rhs: u128) -> Parity { - let res = lhs + rhs; - if (res & 1) == 0 { - Parity::Even(res) - } else { - Parity::Odd(res) -} } + let res = lhs + rhs; + if (res & 1) == 0 { + Parity::Even(res) + } else { + Parity::Odd(res) + } +} ``` -Let's see how it is executed. We start with the following Rust code: +First, we need to compile the program to Sierra and then MLIR: ```rust,ignore -let program = get_sierra_program(); // The result of the `cairo-compile` program. -let module = get_native_module(&program); // This compiles the Sierra program to - // MLIR (not covered here). +// Compile the Cairo to Sierra (using the Cairo compiler). +let program = get_sierra_program(); + +// Compile the Sierra to MLIR (using Cairo native, not covered here). +let module = get_native_module(&program); ``` ## Execution engine preparation -Given a compiled Cairo program in an MLIR module, once it is lowered to the LLVM dialect we have two options to execute it: AOT and JIT. + +Once we have the lowered MLIR module (using only the LLVM dialect) we can +instantiate an execution engine. + +There's two kind of execution engines: + +- The just-in-time (JIT) engine: Generates machine code on the fly. Can be + optimized further taking into account hot paths and other metrics. +- The ahead-of-time (AOT) engine: Uses pre-generated machine code. Has lower + overhead because the machine code is fixed and already compiled, but cannot be + optimized further. ### Using the JIT executor -If we decide to use the JIT executor we just create the jit runner and we're done. + +Using the JIT executor is the easiest option, since we just need to create it +and we're done: ```rust,ignore let program = get_sierra_program(); let module = get_native_module(&program); -// The optimization level can be `None`, `Less`, `Default` or `Aggressive`. They -// are equivalent to compiling a C program using `-O0`, `-O1`, `-O2` and `-O3` -// respectively. +// The JIT engine accepts an optimization level. The available optimization +// levels are: +// - `OptLevel::None`: Applies no optimization (other than what's already been +// optimized by earlier passes). +// - `OptLevel::Less`: Uses only a reduced set of optimizations. +// - `OptLevel::Default`: The default. +// - `OptLevel::Aggressive`: Tries to apply all the (safe) optimizations. +// They're equivalent to using `-O0`, `-O1`, `-O2` and `-O3` when compiling +// C/C++ respectively. let engine = JitNativeExecutor::from_native_module(module, OptLevel::Default); ``` ### Using the AOT executor -Preparing the AOT executor is more complicated since we need to compile it into a shared library and load it from disk. + +Using the AOT executor is a bit more complicated because we need to compile it +into a shared library on disk, but all that complexity has been hidden within +the `AotNativeExecutor::from_native_module` method: ```rust,ignore let program = get_sierra_program(); let module = get_native_module(&program); -// Internally, this method will run all the steps mentioned before internally into -// temporary files and return a working `AotNativeExecutor`. +// Check out the previous section for information about `OptLevel`. let engine = AotNativeExecutor::from_native_module(module, OptLevel::Default); ``` -### Using caches -You can use caches to keep the compiled programs in memory or disk and reuse them between runs. You may use the `ProgramCache` type, or alternatively just `AotProgramCache` or `JitProgramCache` directly. +### Caching the compiled programs -Adding programs to the program cache involves steps not covered here, but once they're inserted you can get executors like this: +Some use cases may benefit from storing the final (machine code) programs. Both +the JIT and AOT programs can be cached within the same process using the +`JitProgramCache` or `AotProgramCache` respectively, or just `ProgramCache` for +a cache that supports both. However, only the AOT supports persisting programs +between runs. They are stored using a different API from the `AotProgramCache`. ```rust,ignore -let engine = program_cache.get(key).expect("program not found"); +// An `Option<...>` is returned, indicating whether the program was present or +// not. +let executor = program_cache.get(key).unwrap(); ``` ## Invoking the program -Regardless of whether we decided to go with AOT or JIT, the program invocation involves the exact same steps. We need to know the entrypoint that we'll be calling and its arguments. -In a future we may be able to implement compile-time trampolines for known program signatures, but for now we need to call the `invoke_dynamic` or `invoke_dynamic_with_syscall_handler` methods which works with any signature. +Invoking the program involves the same steps for both AOT and JIT executors. +There are various methods that may help with invoking both normal programs and +Starknet contracts: -> Note: A trampoline is a function that invokes an compiled MLIR function from Rust code.], +- `invoke_dynamic`: Call into a normal program that doesn't require a syscall + handler. +- `invoke_dynamic_with_syscall_handler`: Same as before, but providing a syscall + handler in case the program needs it. +- `invoke_contract_dynamic`: Call a contract's entry point. It accepts the entry + point's ABI (a span of felts) instead of `Value`s and requires a syscall + handler. -Now we need to find the function id: +There's an extra, more performant way to invoke programs and contracts when we +know the exact signature of the function: we should obtain the function pointer, +cast it into an `extern "C" fn(...) -> ...` and invoke it directly from Rust. It +requires the user to convert the inputs and outputs into/from the expected +internal representation, and to manage the builtins manually. Because of that, +it has not been covered here. -```rust,ignore -let program = get_sierra_program(); - -// The utility function needs the symbol of the entry point, which is built as -// follows: -// ::::() -// -// The `` comes from the Sierra program. It's the index of the -// function in the function declaration section. -let function_id = find_function_id(&program, "program::program::main(f0)"); -``` +All those methods for invoking the program need to know which entrypoint we're +trying to call. We can use the Sierra's function id directly. -The arguments must be placed in a list of `JitValue` instances. The builtins should be ignored since they are filled in automatically. The only builtins required are the `GasBuiltin` and `System` (aka. the syscall handler). They are only mandatory when required by the program itself. +Then we'll need the arguments. Since they can have any supported type in any +order we need to wrap them all in `Value`s and send those to the invoke method. +Builtins are automatically added by the invoke method and should be skipped. ```rust,ignore let engine = get_execution_engine(); // This creates the execution engine (covered before). let args = [ - JitValue::Uint128(1234), - JitValue::Uint128(4321), + Value::Uint128(1234), + Value::Uint128(4321), ]; ``` -> Note: Although it's called `JitValue` for now, it's not tied in any way to the JIT engine. `JitValue`s are used for both the AOT and JIT engines.], - Finally we can invoke the program like this: ```rust,ignore @@ -106,8 +138,8 @@ let engine = get_execution_engine(); let function_id = find_function_id(&program, "program::program::main(f0)"); let args = [ - JitValue::Uint128(1234), - JitValue::Uint128(4321), + Value::Uint128(1234), + Value::Uint128(4321), ]; let execution_result = engine.invoke_dynamic( @@ -155,15 +187,21 @@ Builtin stats: BuiltinStats { bitwise: 1, ec_op: 0, range_check: 1, pedersen: 0, ``` ### Contracts -Contracts always have the same interface, therefore they have an alternative to `invoke_dynamic` called `invoke_contract_dynamic`. + +Contracts always have the same interface, therefore they have an alternative to +`invoke_dynamic` called `invoke_contract_dynamic`. ```rust,ignore fn(Span) -> PanicResult>; ``` -This wrapper will attempt to deserialize the real contract arguments from the span of felts, invoke the contracts, and finally serialize and return the result. When this deserialization fails, the contract will panic with the mythical `Failed to deserialize param #N` error. +This wrapper will attempt to deserialize the real contract arguments from the +span of felts, invoke the contracts, and finally serialize and return the +result. When this deserialization fails, the contract will panic with the +mythical `Failed to deserialize param #N` error. -If the example program had the same interface as a contract (a span of felts) then it'd be invoked like this: +If the example program had the same interface as a contract (a span of felts) +then it'd be invoked like this: ```rust,ignore let engine = get_execution_engine(); @@ -204,27 +242,55 @@ Builtin stats: BuiltinStats { bitwise: 1, ec_op: 0, range_check: 1, pedersen: 0, ``` ## The Cairo Native runtime -Sometimes we need to use stuff that would be too complicated or error-prone to implement in MLIR, but that we have readily available from Rust. That's when we use the runtime library. -When using the JIT it'll be automatically linked (if compiled with support for it, which is enabled by default). If using the AOT, the `CAIRO_NATIVE_RUNTIME_LIBRARY` environment variable will have to be modified to point to the `libcairo_native_runtime.a` file, which is built and placed in said folder by `make build`. +Sometimes we need to use stuff that would be too complicated or error-prone to +implement in MLIR, but that we have readily available from Rust. That's when we +use the runtime library. -Although it's implemented in Rust, its functions use the C ABI and have Rust's name mangling disabled. This means that to the extern observer it's technically indistinguishible from a library written in C. By doing this we're making the functions callable from MLIR. +When using the JIT it'll be automatically linked (if compiled with support for +it, which is enabled by default). If using the AOT, the +`CAIRO_NATIVE_RUNTIME_LIBRARY` environment variable will have to be modified to +point to the `libcairo_native_runtime.a` file, which is built and placed in said +folder by `make build`. + +Although it's implemented in Rust, its functions use the C ABI and have Rust's +name mangling disabled. This means that to the extern observer it's technically +indistinguishible from a library written in C. By doing this we're making the +functions callable from MLIR. ### Syscall handlers -The syscall handler is similar to the runtime in the sense that we have C-compatible functions called from MLIR, but it's different in that they're built into Cairo Native itself rather than an external library, and that their implementation is user-dependent. -To allow for user-provided syscall handler implementations we pass a pointer to a vtable every time we detect a `System` builtin. We need a vtable and cannot use function names because the methods themselves are generic over the syscall handler implementation. +The syscall handler is similar to the runtime in the sense that we have +C-compatible functions called from MLIR, but it's different in that they're +built into Cairo Native itself rather than an external library, and that their +implementation is user-dependent. + +To allow for user-provided syscall handler implementations we pass a pointer to +a vtable every time we detect a `System` builtin. We need a vtable and cannot +use function names because the methods themselves are generic over the syscall +handler implementation. -> Note: The `System` is used only for syscalls; every syscall has it, therefore it's a perfect candidate for this use. +> Note: The `System` is used only for syscalls; every syscall has it, therefore +> it's a perfect candidate for this use. -Those wrappers then receive a mutable reference to the syscall handler implementation. They are responsible of converting the MLIR-compatible inputs to the Rust representations, calling the implementation, and then converting the results back into MLIR-compatible formats. +Those wrappers then receive a mutable reference to the syscall handler +implementation. They are responsible of converting the MLIR-compatible inputs to +the Rust representations, calling the implementation, and then converting the +results back into MLIR-compatible formats. -This means that as far as the user is concerned, writing a syscall handler is equivalent to implementing the trait `StarknetSyscallHandler` for a custom type. +This means that as far as the user is concerned, writing a syscall handler is +equivalent to implementing the trait `StarknetSyscallHandler` for a custom type. ## Appendix: The C ABI and the trampoline -Normally, calling FFI functions in Rust is as easy as defining an extern function using C-compatible types. We can't do this here because we don't know the function's signature. -It all boils down to the [SystemV ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) in `x86_64` or its equivalent for ARM. Both of them are really similar: +Normally, calling FFI functions in Rust is as easy as defining an extern +function using C-compatible types. We can't do this here because we don't know +the function's signature. + +It all boils down to the +[SystemV ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf) in +`x86_64` or its equivalent for ARM. Both of them are really similar: + - The stack must be aligned to 16 bytes before calling. - Function arguments are spread between some registers and the stack. - Return values use either a few registers or require a pointer. @@ -234,79 +300,103 @@ There's a few other quirks, like which registers are caller vs callee-saved, but ### Arguments Argument location in `x86_64`: -| # | Reg. | Description | +| # | Reg. | Description | |----|-------|------------------------| -| 1 | rdi | A single 64-bit value. | -| 2 | rsi | A single 64-bit value. | -| 3 | rdx | A single 64-bit value. | -| 4 | rcx | A single 64-bit value. | -| 5 | r8 | A single 64-bit value. | -| 6 | r9 | A single 64-bit value. | -| 7+ | Stack | Everything else. | +| 1 | rdi | A single 64-bit value. | +| 2 | rsi | A single 64-bit value. | +| 3 | rdx | A single 64-bit value. | +| 4 | rcx | A single 64-bit value. | +| 5 | r8 | A single 64-bit value. | +| 6 | r9 | A single 64-bit value. | +| 7+ | Stack | Everything else. | Argument location in `aarch64`: -| # | Reg. | Description | -|----|-------|------------------------| -| 1 | x0 | A single 64-bit value. | -| 2 | x1 | A single 64-bit value. | -| 3 | x2 | A single 64-bit value. | -| 4 | x3 | A single 64-bit value. | -| 5 | x4 | A single 64-bit value. | -| 6 | x5 | A single 64-bit value. | -| 7 | x6 | A single 64-bit value. | -| 8 | x7 | A single 64-bit value. | -| 9+ | Stack | Everything else. | - -Usually function calls have arguments of types other than just 64-bit integers. In those cases, for values smaller than 64 bits the smaller register variants are written. For values larger than 64 bits the value is split into multiple registers, but there's a catch: if when splitting the value only one value would remain in registers then that register is padded and the entire value goes into the stack. For example, an `u128` that would be split between registers and the stack is always padded and written entirely in the stack. - -For complex values like structs, the types are flattened into a list of values when written into registers, or just written into the stack the same way they would be written into memory (aka. with the correct alignment, etc). +| # | Reg. | Description | +| --- | ----- | ---------------------- | +| 1 | x0 | A single 64-bit value. | +| 2 | x1 | A single 64-bit value. | +| 3 | x2 | A single 64-bit value. | +| 4 | x3 | A single 64-bit value. | +| 5 | x4 | A single 64-bit value. | +| 6 | x5 | A single 64-bit value. | +| 7 | x6 | A single 64-bit value. | +| 8 | x7 | A single 64-bit value. | +| 9+ | Stack | Everything else. | + +Usually function calls have arguments of types other than just 64-bit integers. +In those cases, for values smaller than 64 bits the smaller register variants +are written. For values larger than 64 bits the value is split into multiple +registers, but there's a catch: if when splitting the value only one value would +remain in registers then that register is padded and the entire value goes into +the stack. For example, an `u128` that would be split between registers and the +stack is always padded and written entirely in the stack. + +For complex values like structs, the types are flattened into a list of values +when written into registers, or just written into the stack the same way they +would be written into memory (aka. with the correct alignment, etc). ### Return values -As mentioned before, return values may be either returned in registers or memory (most likely the stack, but not necessarily). + +As mentioned before, return values may be either returned in registers or memory +(most likely the stack, but not necessarily). Argument location in `x86_64`: -| # | Reg | Description | -|---|-----|-----------------------------| -| 1 | rax | A single 64-bit value. | -| 2 | rdx | The "continuation" of `rax` | +| # | Reg | Description | +| --- | --- | --------------------------- | +| 1 | rax | A single 64-bit value. | +| 2 | rdx | The "continuation" of `rax` | Argument location in `aarch64`: -| # | Reg | Description | -|---|-----|----------------------------| -| 1 | x0 | A single 64-bit value | -| 2 | x1 | The "continuation" of `x0` | -| 3 | x2 | The "continuation" of `x1` | -| 4 | x3 | The "continuation" of `x2` | +| # | Reg | Description | +| --- | --- | -------------------------- | +| 1 | x0 | A single 64-bit value | +| 2 | x1 | The "continuation" of `x0` | +| 3 | x2 | The "continuation" of `x1` | +| 4 | x3 | The "continuation" of `x2` | -Values are different that arguments in that only a single value is returned. If more than a single value needs to be returned then it'll use a pointer. +Values are different that arguments in that only a single value is returned. If +more than a single value needs to be returned then it'll use a pointer. -When a pointer is involved we need to pass it as the first argument. This means that every actual argument has to be shifted down one slot, pushing more stuff into the stack in the process. +When a pointer is involved we need to pass it as the first argument. This means +that every actual argument has to be shifted down one slot, pushing more stuff +into the stack in the process. ### The trampoline -We cannot really influence what values are in the register or the stack from Rust, therefore we need something written in assembler to put everything into place and invoke the function pointer. -This is where the trampoline comes in. It's a simple assembler function that does three things: -1. Fill in the 6 or 8 argument registers with the first values in the data pointer and copy the rest into the stack as-is (no stack alignment or anything, we guarantee from the Rust side that the stack will end up properly aligned). +We cannot really influence what values are in the register or the stack from +Rust, therefore we need something written in assembler to put everything into +place and invoke the function pointer. + +This is where the trampoline comes in. It's a simple assembler function that +does three things: + +1. Fill in the 6 or 8 argument registers with the first values in the data + pointer and copy the rest into the stack as-is (no stack alignment or anything, + we guarantee from the Rust side that the stack will end up properly aligned). 2. Invoke the function pointer. 3. Write the return values (in registers only) into the return pointer. -This function always has the same signature, which is C-compatible, and therefore can be used with Rust's FFI facilities without problems. +This function always has the same signature, which is C-compatible, and +therefore can be used with Rust's FFI facilities without problems. #### AOT calling convention: ##### Arguments + - Written on registers, then the stack. - Structs' fields are treated as individual arguments (flattened). - Enums are structs internally, therefore they are also flattened (including the padding). - The default payload works as expected since it has the correct signature. - - All other payloads require breaking it down into bytes and scattering it through the padding - and default payload's space. + - All other payloads require breaking it down into bytes and scattering it + through the padding and default payload's space. ##### Return values + - Indivisible values that do not fit within a single register (ex. felt252) use multiple registers (x0-x3 for felt252). - Struct arguments, etc... use the stack. -In other words, complex values require a return pointer while simple values do not but may still use multiple registers if they don't fit within one. +In other words, complex values require a return pointer while simple values do +not but may still use multiple registers if they don't fit within one. From 03bce62eb55d06ffdc660b12057f95607da8598f Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 12 Jun 2025 15:32:39 -0300 Subject: [PATCH 03/35] Minor changes --- README.md | 17 +++++++++++++++-- docs/gas_builtin_accounting.md | 16 ++++++++++++---- docs/implementing_libfuncs.md | 9 +++++++-- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6f50d3e793..398acf49f2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ use. This can be done by adding `cairo-native = "0.5.0-rc.6"` to your Cargo.toml ## Getting Started ### Dependencies + - Linux or macOS (aarch64 included) only for now - LLVM 19 with MLIR: On debian you can use [apt.llvm.org](https://apt.llvm.org/), on macOS you can use brew @@ -54,6 +55,7 @@ use. This can be done by adding `cairo-native = "0.5.0-rc.6"` to your Cargo.toml - Git ### Setup + > This step applies to all operating systems. Run the following make target to install the dependencies (**both Linux and macOS**): @@ -63,6 +65,7 @@ make deps ``` #### Linux + Since Linux distributions change widely, you need to install LLVM 19 via your package manager, compile it or check if the current release has a Linux binary. @@ -125,6 +128,7 @@ source env.sh ``` #### MacOS + The makefile `deps` target (which you should have ran before) installs LLVM 19 with brew for you, afterwards you need to execute the `env.sh` script to setup the needed environment variables. @@ -134,6 +138,7 @@ source env.sh ``` ### Make targets: + Running `make` by itself will check whether the required LLVM installation and corelib is found, and then list available targets. @@ -162,11 +167,11 @@ Usage: ``` ## Included Tools + Aside from the compilation and execution engine library, Cairo Native includes a few command-line tools to aid development, and some useful scripts. -These are: -- The contents of the `/scripts/` folder +These are the contents of the `/src/bin` folder - `cairo-native-compile` - `cairo-native-dump` - `cairo-native-run` @@ -176,6 +181,7 @@ These are: - `scarb-native-test` ### `cairo-native-compile` + ```bash Compiles a Cairo project outputting the generated MLIR and the shared library. Exits with 1 if the compilation or run fails, otherwise 0. @@ -197,6 +203,7 @@ Options: ``` ### `cairo-native-dump` + ```bash Usage: cairo-native-dump [OPTIONS] @@ -210,6 +217,7 @@ Options: ``` ### `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. @@ -234,6 +242,7 @@ Options: ``` ### `cairo-native-test` + This tool mimics the `cairo-test` [tool](https://github.com/starkware-libs/cairo/tree/main/crates/cairo-lang-test-runner) and is identical to it in interface, the only feature it doesn't have is the profiler. @@ -275,6 +284,7 @@ cairo-native-test ./cairo-tests/ This will run all the tests (functions marked with the `#[test]` attribute). ### `cairo-native-stress` + This tool runs a stress test on Cairo Native. ```bash @@ -315,6 +325,7 @@ make stress-clean ``` ### `scarb-native-dump` + This tool mimics the `scarb build` [command](https://github.com/software-mansion/scarb/tree/main/extensions/scarb-cairo-test). You can download it on our [releases](https://github.com/lambdaclass/cairo_native/releases) page. @@ -323,6 +334,7 @@ behave like `scarb build`, leaving the MLIR files under the `target/` folder besides the generated JSON sierra files. ### `scarb-native-test` + This tool mimics the `scarb test` [command](https://github.com/software-mansion/scarb/tree/main/extensions/scarb-cairo-test). You can download it on our [releases](https://github.com/lambdaclass/cairo_native/releases) page. @@ -348,6 +360,7 @@ Options: ## Benchmarking ### Requirements + - [hyperfine](https://github.com/sharkdp/hyperfine): `cargo install hyperfine` - [cairo 2.12.0-dev.0](https://github.com/starkware-libs/cairo) - Cairo Corelibs diff --git a/docs/gas_builtin_accounting.md b/docs/gas_builtin_accounting.md index 8f9cccb3e8..1428db29d2 100644 --- a/docs/gas_builtin_accounting.md +++ b/docs/gas_builtin_accounting.md @@ -6,10 +6,11 @@ gas and builtins during execution. ## Gas ### Introduction + Gas management in a blockchain environment involves accounting for the amount of computation performed during the execution of a transaction. This is used to accurately charge the user at the end of the execution or to revert early -if the transaction consumes more gas than provided by the sender. +if the transaction consumes more gas than what it was provided by the sender. This documentation assumes prior knowledge about Sierra and about the way gas accounting is performed in Sierra. For those seeking to deepen their @@ -19,11 +20,13 @@ and greged’s about [gas accounting in Sierra](https://blog.kakarot.org/understanding-sierra-gas-accounting-19d6141d28b9). ### Gas builtin + The gas builtin is used in Sierra in order to perform gas accounting. It is passed as an input to all function calls and holds the current remaining gas. It is represented in MLIR by a simple `u64`. ### Gas metadata + The process of calculating gas begins at the very outset of the compilation process. During the initial setup of the Sierra program, metadata about the program, including gas information, is extracted. Using gas helper functions @@ -32,14 +35,15 @@ the consumed cost (steps, memory holes, builtins usage) for each statement in the Sierra code is stored in a HashMap. ### Withdrawing gas + The action of withdrawing gas can be split in two steps: -- **Calculating Total Gas Cost**: Using the previously constructed HashMap, +1. **Calculating Total Gas Cost**: Using the previously constructed HashMap, we iterate over the various cost tokens (including steps, built-in usage, and memory holes) for the statement, convert them into a [common gas unit](https://github.com/starkware-libs/cairo/blob/v2.7.1/crates/cairo-lang-runner/src/lib.rs#L136), and sum them up to get the total gas cost for the statement. -- **Executing Gas Withdrawal**: The previously calculated gas cost is used +2. **Executing Gas Withdrawal**: The previously calculated gas cost is used when the current statement is a `withdraw_gas` libfunc call. The `withdraw_gas` libfunc takes the current leftover gas as input and uses @@ -50,6 +54,7 @@ branches based on whether the remaining gas is greater than or equal to the amount being withdrawn. ### Example + Let's illustrate this with a simple example using the following Cairo 1 code: ```rust,ignore @@ -119,13 +124,14 @@ llvm.func @"test::test::run_test[expr16](f0)"(%arg0: i64 loc(unknown), %arg1: i1 ``` Here, we see the constant `2680` defined at the begining of the function. -In basic block 1, the withdraw_gas operations are performed: by comparing +In basic block 1, the `withdraw_gas` operations are performed: by comparing `%28` (remaining gas) and `%13` (gas cost), the result stored in `%32` determines the conditional branching. A saturating subtraction between the remaining gas and the gas cost is then performed, updating the remaining gas in the IR. ### Final gas usage + The final gas usage can be easily retrieved from the gas builtin value returned by the function. This is accomplished when [parsing the return values](https://github.com/lambdaclass/cairo_native/blob/65face8194054b7ed396a34a60e7b1595197543a/src/executor.rs#L286) @@ -161,6 +167,7 @@ gas accounting. ## Builtins Counter ### Introduction + The Cairo Native compiler records the usage of each builtins in order to provide information about the program's builtins consumption. This information is NOT used for the gas calculation, as the gas cost of @@ -172,6 +179,7 @@ Counters are then simply incremented by one each time the builtins are called from within the program. ### Example + Let us consider the following Cairo program which uses the `pedersen` builtin: ```rust,ignore diff --git a/docs/implementing_libfuncs.md b/docs/implementing_libfuncs.md index aaef8d4d20..f5e5a5f83d 100644 --- a/docs/implementing_libfuncs.md +++ b/docs/implementing_libfuncs.md @@ -36,11 +36,13 @@ This trait, located in `src/arch.rs` is implemented currently for aarch64 and x8 In `types.rs` we should also declare whether the type is complex under `is_complex`, whether its a builtin in `is_builtin`, a zst in `is_zst` and define it's layout in the `TypeBuilder` trait implementation. +> [!NOTE] > Complex types are always passed by pointer (both as params and return > values) and require a stack allocation. Examples of complex values include > structs and enums, but not felts since LLVM considers them integers. ### Deserializing a type + When **deserializing** (a.k.a converting the inputs so the runner accepts them), you are passed a bump allocator arena from `Bumpalo`, the general idea is to get the layout and size of the type, allocate it under @@ -50,8 +52,9 @@ arena and not Rust itself. This is done in the `to_ptr` method. ### Serializing a type + When **serializing** a type, you will get a `ptr: NonNull<()>` (non null -pointer), which you will have to cast, dereference and then deserialize. +pointer), which you will have to cast, dereference and then serialize. For a simple type to learn how it works, we recommend checking `src/values.rs`, in the `from_ptr` method, look the u8 type in the match, for more complex types, check the felt252 type. @@ -59,6 +62,7 @@ The hardest types to understand are the enums, dictionaries and arrays, since they are complex types. ### Implementing the library function + Libfuncs are implemented under `src/libfuncs.rs` and `src/libfuncs/{libfunc_name}.rs`. Just like types. @@ -98,7 +102,8 @@ After implementing the libfuncs, we need to hookup the `build` method in the `src/libfuncs.rs` match statement. ### Example libfunc implementation: u8_to_felt252 -An example libfunc, converting a u8 to a felt252, extensively commented: + +An example libfunc, converting a `u8` to a `felt252`, extensively commented: ```rust,ignore /// Generate MLIR operations for the `u8_to_felt252` libfunc. From 655d438c197caba9467ccb59291abe10a9961dbf Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 12 Jun 2025 17:10:36 -0300 Subject: [PATCH 04/35] More minor changes --- docs/mlir.md | 3 +++ docs/overview.md | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/mlir.md b/docs/mlir.md index cf342235f8..1835f7cfcb 100644 --- a/docs/mlir.md +++ b/docs/mlir.md @@ -3,6 +3,7 @@ Check out the new MLIR Workshop: https://lambdaclass.github.io/mlir-workshop/ ## How MLIR Works + MLIR is composed of **dialects**, which is like a IR of it's own, and this IR can be converted to another dialect IR (if the functionality exists). This is what makes MLIR shine. @@ -12,6 +13,7 @@ Some commonly used dialects in this project: - The cf dialect: It contains basic control flow operations, such as the `br` and `cond_br`, which are unconditional and conditional jumps. ### The IR + The MLIR IR is composed recursively like this: `Operation -> Region -> Block -> Operations` Each operation has 1 or more region, each region has 1 or more blocks, each @@ -20,6 +22,7 @@ block has 1 or more operations. This way a MLIR program can be composed. ### Transformations and passes + MLIR provides a set of transformations that can optimize the IR. Such as `canonicalize`. diff --git a/docs/overview.md b/docs/overview.md index 15409d79b0..cbb0845b46 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -45,7 +45,7 @@ Then you are free to go and make a PR! ## High level project overview This will explain how the project is structured, without going into much details -yet: +yet. ### Project dependencies @@ -86,22 +86,22 @@ The code is laid out in the following sections: src ├─ arch.rs Trampoline assembly for calling functions with dynamic signatures. ├─ arch/ Architecture-specific code for the trampoline. - ├─ bin/ Binary programs + ├─ bin/ Binary programs. ├─ block_ext.rs A melior (MLIR) block trait extension to write less code. ├─ cache.rs Types and implementations of compiled program caches. ├─ compiler.rs The glue code of the compiler, has the codegen for the function signatures and calls the libfunc codegen implementations. ├─ context.rs The MLIR context wrapper, provides the compile method. - ├─ debug.rs + ├─ debug.rs Debug function that maps the libfuncs to their name. ├─ docs.rs Documentation modules. - ├─ error.rs Error handling, + ├─ error.rs Error handling. ├─ execution_result.rs Program result parsing. - ├─ executor.rs The executor & related code, - ├─ ffi.cpp Missing FFI C wrappers, + ├─ executor.rs The executor & related code. + ├─ ffi.cpp Missing FFI C wrappers. ├─ ffi.rs Missing FFI C wrappers, rust side. ├─ lib.rs The main lib file. - ├─ libfuncs.rs Cairo Sierra libfunc glue code & implementations, + ├─ libfuncs.rs Cairo Sierra libfunc glue code & implementations. ├─ metadata.rs Metadata injector to use within the compilation process. ├─ module.rs The MLIR module wrapper. ├─ starknet.rs Starknet syscall handler glue code. From 5fc6372db3c856dfcd67c7edcd075d649eabe641 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 12 Jun 2025 17:52:21 -0300 Subject: [PATCH 05/35] Refactor on src layout --- docs/overview.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/overview.md b/docs/overview.md index cbb0845b46..a9a17dcfbb 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -87,7 +87,8 @@ The code is laid out in the following sections: ├─ arch.rs Trampoline assembly for calling functions with dynamic signatures. ├─ arch/ Architecture-specific code for the trampoline. ├─ bin/ Binary programs. - ├─ block_ext.rs A melior (MLIR) block trait extension to write less code. + ├─ utils/ Utily traits and methods like a melior (MLIR) block trait + extension to write less code. ├─ cache.rs Types and implementations of compiled program caches. ├─ compiler.rs The glue code of the compiler, has the codegen for the function signatures and calls the libfunc From b6ada90d45a218a8515b9f4d0bbc116e7ee7f484 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Fri, 13 Jun 2025 09:46:04 -0300 Subject: [PATCH 06/35] Update examples on overview.md --- docs/overview.md | 59 +++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index a9a17dcfbb..d1184ac474 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -145,10 +145,10 @@ development, such as wrapping return values and printing them. ## Basic API usage example -The API contains two structs, `NativeContext` and `NativeExecutor`. +The API contains three structs, `NativeContext`, `JitNativeExecutor` and `AotNativeExecutor`. The main purpose of `NativeContext` is MLIR initialization, compilation and lowering to LLVM. -`NativeExecutor` in the other hand is responsible of executing MLIR +The two variants of native executors in the other hand are responsible of executing MLIR compiled sierra programs from an entrypoint. Programs and JIT states can be cached in contexts where their execution will be done multiple times. @@ -159,34 +159,36 @@ use cairo_native::executor::JitNativeExecutor; use cairo_native::values::JitValue; use std::path::Path; -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); +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(); + // 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, None).unwrap(); + // Compile the sierra program into a MLIR module. + let native_program = native_context.compile(&sierra_program, true, None, None).unwrap(); -// The parameters of the entry point. -let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))]; + // 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); + // 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 = JitNativeExecutor::from_native_module(native_program, Default::default()); + // Instantiate the executor. + let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()); -// Execute the program. -let result = native_executor - .invoke_dynamic(entry_point_id, params, None) - .unwrap(); + // Execute the program. + let result = native_executor + .invoke_dynamic(entry_point_id, params, None) + .unwrap(); -println!("Cairo program was compiled and executed successfully."); -println!("{:?}", result); + println!("Cairo program was compiled and executed successfully."); + println!("{:?}", result); +} ``` ## Running a Cairo program @@ -200,7 +202,7 @@ Example code to run a program: ```rust,ignore use starknet_types_core::felt::Felt; use cairo_native::context::NativeContext; -use cairo_native::executor::NativeExecutor; +use cairo_native::executor::JitNativeExecutor; use cairo_native::values::JitValue; use std::path::Path; @@ -214,7 +216,7 @@ fn main() { let native_context = NativeContext::new(); // Compile the sierra program into a MLIR module. - let native_program = native_context.compile(&sierra_program).unwrap(); + let native_program = native_context.compile(&sierra_program, true, None, None).unwrap(); // The parameters of the entry point. let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))]; @@ -224,7 +226,7 @@ fn main() { let entry_point_id = cairo_native::utils::find_function_id(&sierra_program, entry_point); // Instantiate the executor. - let native_executor = NativeExecutor::new(native_program); + let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()); // Execute the program. let result = native_executor @@ -287,14 +289,15 @@ fn main() { let fn_id = &entry_point_fn.id; - let native_executor = NativeExecutor::new(native_program); + let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()); let result = native_executor - .execute_contract( + .invoke_contract_dynamic( fn_id, // The calldata &[JitValue::Felt252(Felt::ONE)], u64::MAX.into(), + SyscallHandler::new() ) .expect("failed to execute the given contract"); From 6cd39dd88577214520f5831021a248c421ab18f3 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Fri, 13 Jun 2025 10:15:18 -0300 Subject: [PATCH 07/35] Minor changes on overview.md example --- docs/overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index d1184ac474..ded0c1e7cd 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -172,11 +172,11 @@ fn main() { let native_program = native_context.compile(&sierra_program, true, None, None).unwrap(); // The parameters of the entry point. - let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))]; + let params = &[Value::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); + let entry_point_id = cairo_native::utils::find_function_id(&sierra_program, entry_point).expect("entry point not found"); // Instantiate the executor. let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()); From f36784d11ea8d51c15178939067fe6627bfefc6e Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Fri, 13 Jun 2025 10:16:30 -0300 Subject: [PATCH 08/35] Minor change --- docs/overview.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index ded0c1e7cd..75068f560d 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -278,7 +278,7 @@ fn main() { let native_context = NativeContext::new(); - let mut native_program = native_context.compile(&sierra_program).unwrap(); + let mut native_program = native_context.compile(&sierra_program, false, Some(Default::default()), None).unwrap(); native_program .insert_metadata(SyscallHandlerMeta::new(&mut SyscallHandler)) .unwrap(); @@ -295,8 +295,8 @@ fn main() { .invoke_contract_dynamic( fn_id, // The calldata - &[JitValue::Felt252(Felt::ONE)], - u64::MAX.into(), + &[Felt::ONE], + Some(u64::MAX), SyscallHandler::new() ) .expect("failed to execute the given contract"); From 107ee68bf2aca7b3dcb5782f430ea6e99e9a39f2 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Fri, 13 Jun 2025 18:18:01 -0300 Subject: [PATCH 09/35] Remove wrong enum explanation --- docs/gas_builtin_accounting.md | 2 +- docs/overview.md | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/gas_builtin_accounting.md b/docs/gas_builtin_accounting.md index 1428db29d2..41e20e87ba 100644 --- a/docs/gas_builtin_accounting.md +++ b/docs/gas_builtin_accounting.md @@ -174,7 +174,7 @@ This information is NOT used for the gas calculation, as the gas cost of builtins is already taken into account during the [gas accounting process](./gas.md). The builtins counter types can each be found in the [types folder](../src/types/). Taking the [Pedersen hash](../src/types/pedersen.rs) as an example, we see -that the counters will be represented as i64 integers in MLIR. +that the counters will be represented as `i64` integers in MLIR. Counters are then simply incremented by one each time the builtins are called from within the program. diff --git a/docs/overview.md b/docs/overview.md index 75068f560d..c32f8a0db3 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -119,17 +119,6 @@ Path: `src/libfuncs` Here are stored all the library function implementations in MLIR, this contains the majority of the code. -To store information about the different types of library functions sierra -has, we divide them into the following using the enum `SierraLibFunc`: - -- **Branching**: These functions are implemented inline, adding blocks and - jumping as necessary based on given conditions. -- **Constant**: A constant value, this isn't represented as a function and - is inserted inline. -- **Function**: Any other function. -- **InlineDataFlow**: Functions that can be implemented inline without much - problem. For example: `dup`, `store_temp` - ### Statements Path: `src/statements` From b2cfadb67a9bc427212c56f8f7c98917e18e6d23 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Fri, 13 Jun 2025 22:00:17 -0300 Subject: [PATCH 10/35] Fix merge mistake --- src/metadata/gas.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/metadata/gas.rs b/src/metadata/gas.rs index 6db4d9c26a..e84b2b77ce 100644 --- a/src/metadata/gas.rs +++ b/src/metadata/gas.rs @@ -16,15 +16,16 @@ use cairo_lang_sierra::{ ids::FunctionId, program::{Program, StatementIdx}, }; -use cairo_lang_sierra_ap_change::{ - ap_change_info::ApChangeInfo, calc_ap_changes, - compute::calc_ap_changes as linear_calc_ap_changes, ApChangeError, +use cairo_lang_sierra_ap_change::{ap_change_info::ApChangeInfo, ApChangeError}; +use cairo_lang_sierra_gas::{gas_info::GasInfo, CostError}; +use cairo_lang_sierra_to_casm::metadata::{ + calc_metadata, calc_metadata_ap_change_only, Metadata as CairoGasMetadata, + MetadataComputationConfig, MetadataError as CairoGasMetadataError, }; -use cairo_lang_sierra_gas::{ - compute_postcost_info, compute_precost_info, gas_info::GasInfo, CostError, -}; -use cairo_lang_utils::ordered_hash_map::OrderedHashMap; -use std::collections::BTreeMap; + +use crate::{error::Result as NativeResult, native_panic}; + +use std::{collections::BTreeMap, fmt, ops::Deref}; /// Holds global gas info. #[derive(Default)] From a46520907f17dc83a5037b2d2d4436f4095550f5 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Tue, 17 Jun 2025 11:50:09 -0300 Subject: [PATCH 11/35] Remove JitValue references --- docs/execution_walkthrough.md | 8 ++++---- docs/overview.md | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index 47ce224998..7eca222de7 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -144,7 +144,7 @@ let args = [ let execution_result = engine.invoke_dynamic( function_id, // The entry point function id. - args, // The slice of `JitValue`s. + args, // The slice of `Value`s. None, // The available gas (if any). )?; @@ -214,7 +214,7 @@ let args = [ let execution_result = engine.invoke_dynamic( function_id, // The entry point function id. - args, // The slice of `JitValue`s. + args, // The slice of `Value`s. None, // The available gas (if any). )?; @@ -235,8 +235,8 @@ Running the code above should print the following: Remaining gas: None Failure flag: false Return value: [ - JitValue::Felt252(1), - JitValue::Felt252(5555), + Value::Felt252(1), + Value::Felt252(5555), ] Builtin stats: BuiltinStats { bitwise: 1, ec_op: 0, range_check: 1, pedersen: 0, poseidon: 0, segment_arena: 0 } ``` diff --git a/docs/overview.md b/docs/overview.md index c32f8a0db3..aa810adb69 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -145,7 +145,7 @@ cached in contexts where their execution will be done multiple times. use starknet_types_core::felt::Felt; use cairo_native::context::NativeContext; use cairo_native::executor::JitNativeExecutor; -use cairo_native::values::JitValue; +use cairo_native::values::Value; use std::path::Path; fn main() { @@ -192,7 +192,7 @@ Example code to run a program: use starknet_types_core::felt::Felt; use cairo_native::context::NativeContext; use cairo_native::executor::JitNativeExecutor; -use cairo_native::values::JitValue; +use cairo_native::values::Value; use std::path::Path; fn main() { @@ -208,7 +208,7 @@ fn main() { let native_program = native_context.compile(&sierra_program, true, None, None).unwrap(); // The parameters of the entry point. - let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))]; + let params = &[Value::Felt252(Felt::from_bytes_be_slice(b"user"))]; // Find the entry point id by its name. let entry_point = "hello::hello::greet"; @@ -238,7 +238,6 @@ 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}, From 34e5c6a298a8449e2876d02d0516ecbe3f58b21c Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Wed, 18 Jun 2025 11:23:21 -0300 Subject: [PATCH 12/35] Apply comments --- Makefile | 2 +- README.md | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 46cf3973a0..3fe7ed6e8c 100644 --- a/Makefile +++ b/Makefile @@ -141,7 +141,7 @@ endif .PHONY: deps-macos deps-macos: build-cairo-2-compiler-macos install-scarb-macos -brew install llvm@19 --quiet - @echo "You can execute the env-macos.sh script to setup the needed env variables." + @echo "You can execute the env.sh script to setup the needed env variables." # CI use only .PHONY: deps-ci-linux build-cairo-2-compiler install-scarb diff --git a/README.md b/README.md index 398acf49f2..aba3813481 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ to machine code via MLIR and LLVM. - [Getting Started](#getting-started) - [Included Tools](#included-tools) - - [Scripts](#scripts) - [cairo-native-compile](#cairo-native-compile) - [cairo-native-dump](#cairo-native-dump) - [cairo-native-run](#cairo-native-run) @@ -145,7 +144,7 @@ corelib is found, and then list available targets. ```bash % make LLVM is correctly set at /opt/homebrew/opt/llvm. -./scripts/check-corelib-version.sh 2.12.0-dev.0 +./scripts/check-corelib-version.sh 2.12.0-dev.1 Usage: deps: Installs the necesary dependencies. build: Builds the cairo-native library and binaries in release mode. @@ -159,7 +158,7 @@ Usage: doc-open: Builds and opens documentation in browser. bench: Runs the hyperfine benchmark script. bench-ci: Runs the criterion benchmarks for CI. - install: Invokes cargo to install the cairo-native tools. + install: Invokes cargo to install cairo-native tools. clean: Cleans the built artifacts. stress-test Runs a command which runs stress tests. stress-plot Plots the results of the stress test command. @@ -221,7 +220,7 @@ Options: This tool allows to run programs using the JIT engine, like the `cairo-run` tool, the parameters can only be felt values. -Example: `echo '1' | cairo-native-run 'program.cairo' 'program::program::main' --inputs - --outputs -` +Example: `cairo-native-run --available-gas 5000 './programs/program.cairo'` ```bash Exits with 1 if the compilation or run fails, otherwise 0. From 722ea699579823ecc1192a683844a06bb89272b8 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Wed, 18 Jun 2025 17:40:19 -0300 Subject: [PATCH 13/35] Remove outdated link --- docs/gas_builtin_accounting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gas_builtin_accounting.md b/docs/gas_builtin_accounting.md index 41e20e87ba..ace202aab7 100644 --- a/docs/gas_builtin_accounting.md +++ b/docs/gas_builtin_accounting.md @@ -171,7 +171,7 @@ gas accounting. The Cairo Native compiler records the usage of each builtins in order to provide information about the program's builtins consumption. This information is NOT used for the gas calculation, as the gas cost of -builtins is already taken into account during the [gas accounting process](./gas.md). +builtins is already taken into account during the gas accounting process. The builtins counter types can each be found in the [types folder](../src/types/). Taking the [Pedersen hash](../src/types/pedersen.rs) as an example, we see that the counters will be represented as `i64` integers in MLIR. From 5b183170c2fd71b4baca3752ee069cfd7ff88020 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 19 Jun 2025 12:38:23 -0300 Subject: [PATCH 14/35] Change cairo-native-run example file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aba3813481..87a33f7375 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ Options: This tool allows to run programs using the JIT engine, like the `cairo-run` tool, the parameters can only be felt values. -Example: `cairo-native-run --available-gas 5000 './programs/program.cairo'` +Example: `cairo-native-run --available-gas 10000 './programs/array_get.cairo'` ```bash Exits with 1 if the compilation or run fails, otherwise 0. From 5ef2c424770613b9c21e21a5682c681159758500 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 19 Jun 2025 14:57:02 -0300 Subject: [PATCH 15/35] Update runtime info --- docs/execution_walkthrough.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index 7eca222de7..c0c107a9c9 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -244,14 +244,11 @@ Builtin stats: BuiltinStats { bitwise: 1, ec_op: 0, range_check: 1, pedersen: 0, ## The Cairo Native runtime Sometimes we need to use stuff that would be too complicated or error-prone to -implement in MLIR, but that we have readily available from Rust. That's when we -use the runtime library. +implement in MLIR, but that we have readily available from Rust. -When using the JIT it'll be automatically linked (if compiled with support for -it, which is enabled by default). If using the AOT, the -`CAIRO_NATIVE_RUNTIME_LIBRARY` environment variable will have to be modified to -point to the `libcairo_native_runtime.a` file, which is built and placed in said -folder by `make build`. +When initializing an executor, for each of the variants of the `RuntimeBinding` enum a +pointer to a runtime function is created on global. Then on execution, it will access the global +of the desired function and find its pointer. Although it's implemented in Rust, its functions use the C ABI and have Rust's name mangling disabled. This means that to the extern observer it's technically @@ -261,8 +258,7 @@ functions callable from MLIR. ### Syscall handlers The syscall handler is similar to the runtime in the sense that we have -C-compatible functions called from MLIR, but it's different in that they're -built into Cairo Native itself rather than an external library, and that their +C-compatible functions called from MLIR, but it's different in that their implementation is user-dependent. To allow for user-provided syscall handler implementations we pass a pointer to From 49f3d6a680ff1b8a59b66e774050e04718e26e99 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 19 Jun 2025 17:47:22 -0300 Subject: [PATCH 16/35] Update tools help message --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 87a33f7375..977ccdf3d5 100644 --- a/README.md +++ b/README.md @@ -253,33 +253,62 @@ Exits with 1 if the compilation or run fails, otherwise 0. Usage: cairo-native-test [OPTIONS] Arguments: - The Cairo project path to compile and run its tests + + 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 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 with JIT or AOT (compiled) [default: jit] [possible values: aot, jit] - -O, --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 + -s, --single-file + Whether path is a single file + + --allow-warnings + Allows the compilation to succeed with warnings + + -f, --filter + The filter for the tests, running only tests containing the filter string + + [default: ] + + --skip-compilation + Skips compilation for tests/functions containing any of the given filters. Unlike `--filter`, the matching tests are not even compiled by native. + + DISCLAIMER: This is a hacky and temporary flag, used to run corelib tests when not all libfuncs are implemented. + + --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 with JIT or AOT (compiled) + + [default: jit] + [possible values: aot, jit] + + -O, --opt-level + Optimization level, Valid: 0, 1, 2, 3. Values higher than 3 are considered as 3 + + [default: 0] + + -h, --help + Print help (see a summary with '-h') + + -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. - ```bash cairo-native-test -s myfile.cairo - -cairo-native-test ./cairo-tests/ ``` +For a project, it needs to have a `cairo_project.toml` specifying the +`crate_roots`. + This will run all the tests (functions marked with the `#[test]` attribute). ### `cairo-native-stress` @@ -350,6 +379,7 @@ Options: -f, --filter Run only tests whose name contain FILTER [default: ] --include-ignored Run ignored and not ignored tests --ignored Run only ignored tests + -t, --test-kind Choose test kind to run [possible values: unit, integration, all] --run-mode Run with JIT or AOT (compiled) [default: jit] [possible values: aot, jit] -O, --opt-level Optimization level, Valid: 0, 1, 2, 3. Values higher than 3 are considered as 3 [default: 0] -h, --help Print help From c42454ddbea83eb9a841ff9a00895e352942f6b0 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 19 Jun 2025 18:15:34 -0300 Subject: [PATCH 17/35] Undo changes on compile.rs --- src/compiler.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index 571900f343..2e998d2419 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -461,6 +461,7 @@ fn compile_func( |statement_idx, mut state| { if let Some(gas_metadata) = metadata.get::() { let gas_cost = gas_metadata.get_gas_costs_for_statement(statement_idx); + metadata.remove::(); metadata.insert(GasCost(gas_cost)); } @@ -499,7 +500,7 @@ fn compile_func( )); } - let statement_compilation_result = match &statements[statement_idx.0] { + Ok(match &statements[statement_idx.0] { Statement::Invocation(invocation) => { tracing::trace!( "Implementing the invocation statement at {statement_idx}: {}.", @@ -667,7 +668,7 @@ fn compile_func( } } - StatementCompilationResult::Processed( + StatementCompileResult::Processed( invocation .branches .iter() @@ -725,7 +726,7 @@ fn compile_func( // within a tail-recursive function before the recursive call has // been generated. Since we don't have the return target block at // this point we need to defer this return statement's generation. - return Ok(StatementCompilationResult::Deferred); + return Ok(StatementCompileResult::Deferred); } Some((depth_counter, recursion_target)) => { let location = Location::name( @@ -892,13 +893,9 @@ fn compile_func( location, )); - StatementCompilationResult::Processed(Vec::new()) + StatementCompileResult::Processed(Vec::new()) } - }; - - metadata.remove::(); - - Ok(statement_compilation_result) + }) }, )?; @@ -1094,7 +1091,7 @@ fn generate_function_structure<'c, 'a>( } } - StatementCompilationResult::Processed( + StatementCompileResult::Processed( invocation .branches .iter() @@ -1157,7 +1154,7 @@ fn generate_function_structure<'c, 'a>( block.add_argument(ty, location); } - StatementCompilationResult::Processed(Vec::new()) + StatementCompileResult::Processed(Vec::new()) } }) }, @@ -1282,7 +1279,7 @@ fn foreach_statement_in_function( statements: &[Statement], entry_point: StatementIdx, initial_state: S, - mut closure: impl FnMut(StatementIdx, S) -> Result>, E>, + mut closure: impl FnMut(StatementIdx, S) -> Result>, E>, ) -> Result<(), E> where S: Clone, @@ -1296,7 +1293,7 @@ where } match closure(statement_idx, state.clone())? { - StatementCompilationResult::Processed(branch_states) => { + StatementCompileResult::Processed(branch_states) => { let branches = match &statements[statement_idx.0] { Statement::Invocation(x) => x.branches.as_slice(), Statement::Return(_) => &[], @@ -1314,7 +1311,7 @@ where .zip(branch_states), ); } - StatementCompilationResult::Deferred => { + StatementCompileResult::Deferred => { tracing::trace!("Statement {statement_idx}'s compilation has been deferred."); visited.remove(&statement_idx); @@ -1466,7 +1463,7 @@ fn generate_entry_point_wrapper<'c>( /// Return type for the closure in [`foreach_statement_in_function`] that determines whether the /// statement was processed successfully or needs to be processed again at the end. #[derive(Clone, Debug)] -enum StatementCompilationResult { +enum StatementCompileResult { /// The statement was processed successfully. Processed(T), /// The statement's processing has to be deferred until the end. From 8002c24c9e6c99eaa33b58a2b1004daf6f3c690e Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Mon, 23 Jun 2025 10:38:34 -0300 Subject: [PATCH 18/35] Update example on overview.md --- docs/overview.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index aa810adb69..7567e3c770 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -234,13 +234,12 @@ Example code to run a Starknet contract: ```rust,ignore use starknet_types_core::felt::Felt; use cairo_lang_compiler::CompilerConfig; -use cairo_lang_starknet::contract_class::compile_path; +use cairo_lang_starknet::compile::compile_path; use cairo_native::context::NativeContext; -use cairo_native::executor::NativeExecutor; +use cairo_native::executor::JitNativeExecutor; use cairo_native::utils::find_entry_point_by_idx; use cairo_native::{ - metadata::syscall_handler::SyscallHandlerMeta, - starknet::{BlockInfo, ExecutionInfo, StarkNetSyscallHandler, SyscallResult, TxInfo, U256}, + starknet::{BlockInfo, ExecutionInfo, StarknetSyscallHandler, SyscallResult, TxInfo, U256}, }; use std::path::Path; @@ -267,9 +266,6 @@ fn main() { let native_context = NativeContext::new(); let mut native_program = native_context.compile(&sierra_program, false, Some(Default::default()), None).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 = @@ -277,7 +273,7 @@ fn main() { let fn_id = &entry_point_fn.id; - let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()); + let native_executor = JitNativeExecutor::from_native_module(native_program, Default::default()).unwrap(); let result = native_executor .invoke_contract_dynamic( @@ -294,8 +290,14 @@ fn main() { println!("{result:#?}"); } +impl SyscallHandler { + pub fn new() -> Self { + Self {} + } +} + // Implement an example syscall handler. -impl StarkNetSyscallHandler for SyscallHandler { +impl StarknetSyscallHandler for SyscallHandler { fn get_block_hash( &mut self, block_number: u64, @@ -428,7 +430,10 @@ impl StarkNetSyscallHandler for SyscallHandler { _gas: &mut u64, ) -> SyscallResult { println!("Called `keccak({input:?})` from MLIR."); - Ok(U256(Felt::from(1234567890).to_le_bytes())) + Ok(U256 { + hi: 0, + lo: 1234567890, + }) } /* From cac4d3402c73b3c2f91af537c67b8c682c5e8b90 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Mon, 23 Jun 2025 18:13:33 -0300 Subject: [PATCH 19/35] Add AotContractExecutor --- docs/overview.md | 6 +++--- src/context.rs | 2 +- src/executor/contract.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 7567e3c770..09f32b9d80 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -134,9 +134,9 @@ development, such as wrapping return values and printing them. ## Basic API usage example -The API contains three structs, `NativeContext`, `JitNativeExecutor` and `AotNativeExecutor`. -The main purpose of `NativeContext` is MLIR initialization, compilation and -lowering to LLVM. +The API contains four structs, `NativeContext`, `JitNativeExecutor`, `AotNativeExecutor` and `AotContractExecutor`. +The main purpose of `NativeContext` is MLIR initialization, compilation and has some optimizations for their execution. +lowering to LLVM. then we have the `AotContractExecutor` which is specialized for Starknet contracts The two variants of native executors in the other hand are responsible of executing MLIR compiled sierra programs from an entrypoint. Programs and JIT states can be cached in contexts where their execution will be done multiple times. diff --git a/src/context.rs b/src/context.rs index de8802c437..3d723f2f28 100644 --- a/src/context.rs +++ b/src/context.rs @@ -64,7 +64,7 @@ impl NativeContext { /// Returns the corresponding NativeModule struct. /// /// If `ignore_debug_names` is true then debug names will not be added to function names. - /// Mainly useful for the ContractExecutor. + /// Mainly useful for the AotContractExecutor. pub fn compile( &self, program: &Program, diff --git a/src/executor/contract.rs b/src/executor/contract.rs index b53e06a2e1..1ab546d640 100644 --- a/src/executor/contract.rs +++ b/src/executor/contract.rs @@ -13,11 +13,11 @@ //! ## How it works: //! //! The only variable data we need to know at call time is the builtins order, -//! to save this, when first compiling the sierra program (with [`ContractExecutor::new`]) it iterates through all the user +//! to save this, when first compiling the sierra program (with [`AotContractExecutor::new`]) it iterates through all the user //! defined functions (this includes the contract wrappers, which are the ones that matter) //! and saves the builtin arguments in a `Vec`. //! -//! The API provides two more methods: [`ContractExecutor::save`] and [`ContractExecutor::load`]. +//! The API provides two more methods: [`AotContractExecutor::save`] and [`AotContractExecutor::load`]. //! //! Save can be used to save the compiled program into the given path, alongside it will be saved //! a json file with the entry points and their builtins (as seen in the example) From 88b8c877d491ed8e78d08139b3a0db00542a291d Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Tue, 15 Jul 2025 10:39:02 -0300 Subject: [PATCH 20/35] Update sierra-emu references --- docs/debugging.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/debugging.md b/docs/debugging.md index 6ff4e828ed..c8e931f01c 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -367,7 +367,7 @@ Once we know all that we can: ### Comparing with Sierra Emulator -To aid in the debugging process, we developed [sierra-emu](https://github.com/lambdaclass/sierra-emu/). It’s an external tool that executes raw sierra code and outputs an execution trace, containing each statement executed and the associated state. +To aid in the debugging process, we developed [sierra-emu](https://github.com/lambdaclass/cairo_native/tree/main/debug_utils/sierra-emu). It’s a tool that executes raw sierra code and outputs an execution trace, containing each statement executed and the associated state. In addition to this, we developed the `with-trace-dump` feature for Cairo Native, which generates an execution trace that records every statement executed. It has the same shape as the one generated by the Sierra emulator. Supporting transaction execution with Cairo Native trace dump required quite a few hacks, which is why we haven’t merged it to main. This is why we need to use a specific cairo native branch. @@ -402,8 +402,8 @@ If the execution panics, It may indicate that not all the required libfuncs or t ```toml [patch.'https://github.com/lambdaclass/cairo_native'] cairo-native = { path = "../cairo_native" } -[patch.'https://github.com/lambdaclass/sierra-emu'] -sierra-emu = { path = "../sierra-emu" } +[patch.'https://github.com/lambdaclass/cairo_native/tree/main/debug_utils/sierra-emu'] +sierra-emu = { path = "../cairo-native/debug_utils/sierra-emu" } ``` #### Comparing Traces From b755bab57f4058c90303c3c20c1811a6658ebe40 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 17 Jul 2025 09:37:05 -0300 Subject: [PATCH 21/35] API usage refactor --- docs/overview.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 09f32b9d80..6c257d5472 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -135,11 +135,10 @@ development, such as wrapping return values and printing them. ## Basic API usage example The API contains four structs, `NativeContext`, `JitNativeExecutor`, `AotNativeExecutor` and `AotContractExecutor`. -The main purpose of `NativeContext` is MLIR initialization, compilation and has some optimizations for their execution. -lowering to LLVM. then we have the `AotContractExecutor` which is specialized for Starknet contracts -The two variants of native executors in the other hand are responsible of executing MLIR -compiled sierra programs from an entrypoint. Programs and JIT states can be -cached in contexts where their execution will be done multiple times. +The main purpose of `NativeContext` is MLIR initialization and compilation. The two variants +of native executors on the other hand, are responsible of executing MLIR compiled sierra +programs from an entrypoint. Programs and JIT states can be cached in contexts where their +execution will be done multiple times. Finally, we have the AotContractExecutor which is specialized for executing Starknet contracts. ```rust,ignore use starknet_types_core::felt::Felt; From 580521a01d987ae01611de04cedf33f1b0f42678 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 17 Jul 2025 10:06:26 -0300 Subject: [PATCH 22/35] Refactor of project layout --- docs/overview.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 6c257d5472..7f265e1039 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -84,11 +84,15 @@ The code is laid out in the following sections: ```txt src - ├─ arch.rs Trampoline assembly for calling functions with dynamic signatures. ├─ arch/ Architecture-specific code for the trampoline. ├─ bin/ Binary programs. + ├─ cache/ Cache implementations for Jit and Aot. + ├─ executor/ Implementations of the AotNativeExecutor, AotContractExecutor and JitNativeExecutor. + ├─ libfuncs/ Libfuncs implementations + ├─ types/ Cairo type to MLIR implementations. ├─ utils/ Utily traits and methods like a melior (MLIR) block trait extension to write less code. + ├─ arch.rs Trampoline assembly for calling functions with dynamic signatures. ├─ cache.rs Types and implementations of compiled program caches. ├─ compiler.rs The glue code of the compiler, has the codegen for the function signatures and calls the libfunc @@ -105,9 +109,10 @@ The code is laid out in the following sections: ├─ libfuncs.rs Cairo Sierra libfunc glue code & implementations. ├─ metadata.rs Metadata injector to use within the compilation process. ├─ module.rs The MLIR module wrapper. - ├─ starknet.rs Starknet syscall handler glue code. + ├─ runtime.rs Methods that need to use the Rust runtime ├─ starknet_stub.rs - ├─ types.rs Cairo to MLIR type information, + ├─ starknet.rs Starknet syscall handler glue code. + ├─ types.rs Conversion from Sierra to MLIR types. Cairo to MLIR type information. ├─ utils.rs Internal utilities. └─ values.rs JIT serialization. ``` From da5c08d6ad5331723a4ee6e4633bbdca6f572777 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 17 Jul 2025 10:21:12 -0300 Subject: [PATCH 23/35] Add tool info --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 977ccdf3d5..5d4f754136 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ to machine code via MLIR and LLVM. - [cairo-native-stress](#cairo-native-stress) - [scarb-native-dump](#scarb-native-dump) - [scarb-native-test](#scarb-native-test) + - [starknet-native-compile](#starknet-native-compile) - [Benchmarking](#benchmarking) For in-depth documentation, see the [developer documentation][]. @@ -178,6 +179,7 @@ These are the contents of the `/src/bin` folder - `cairo-native-stress` - `scarb-native-dump` - `scarb-native-test` +- `starknet-native-compile` ### `cairo-native-compile` @@ -386,6 +388,26 @@ Options: -V, --version Print version ``` +### `starknet-native-compile` + +```bash +Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program from +felts into readable Sierra code, compiles it to native, and saves the result to the given output +path. + +Usage: starknet-native-compile [OPTIONS] + +Arguments: + The path of the Sierra file to compile + The output file path + +Options: + -O, --opt-level Optimization level, Valid: 0, 1, 2, 3. Values higher than 3 are considered as 3 [default: 0] + --stats Output path for compilation statistics + -h, --help Print help + -V, --version Print version +``` + ## Benchmarking ### Requirements From ee28e905df102e8c4fa565d5f021942e91fca6ab Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 17 Jul 2025 11:08:33 -0300 Subject: [PATCH 24/35] Refactor of runtime explanation --- docs/execution_walkthrough.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index c0c107a9c9..fd2d86c7ed 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -246,9 +246,9 @@ Builtin stats: BuiltinStats { bitwise: 1, ec_op: 0, range_check: 1, pedersen: 0, Sometimes we need to use stuff that would be too complicated or error-prone to implement in MLIR, but that we have readily available from Rust. -When initializing an executor, for each of the variants of the `RuntimeBinding` enum a -pointer to a runtime function is created on global. Then on execution, it will access the global -of the desired function and find its pointer. +When initializing an executor, for each of the variants of the `RuntimeBinding` enum, an MLIR global symbol +is declared that will contain a pointer to the runtime fucntion implementation. Then on execution, it will access the global symbol of the desired function and find its pointer. This way, each contract uses the +same runtime library located on the sequencer. Although it's implemented in Rust, its functions use the C ABI and have Rust's name mangling disabled. This means that to the extern observer it's technically From 28225831e83f6fb7cc8cfaa848c3b8ab9eca7d7b Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Thu, 17 Jul 2025 11:15:33 -0300 Subject: [PATCH 25/35] Minor changes --- docs/execution_walkthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index fd2d86c7ed..286984eb97 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -248,7 +248,7 @@ implement in MLIR, but that we have readily available from Rust. When initializing an executor, for each of the variants of the `RuntimeBinding` enum, an MLIR global symbol is declared that will contain a pointer to the runtime fucntion implementation. Then on execution, it will access the global symbol of the desired function and find its pointer. This way, each contract uses the -same runtime library located on the sequencer. +same runtime library located on the sequencer in a dynamic dispatch way. Although it's implemented in Rust, its functions use the C ABI and have Rust's name mangling disabled. This means that to the extern observer it's technically From 717935e4943227780142b0aebc958f1a08f7edd2 Mon Sep 17 00:00:00 2001 From: DiegoC <90105443+DiegoCivi@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:28:49 -0300 Subject: [PATCH 26/35] Update docs/overview.md Co-authored-by: Julian Gonzalez Calderon --- docs/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview.md b/docs/overview.md index 7f265e1039..3d77ad5afd 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -109,7 +109,7 @@ The code is laid out in the following sections: ├─ libfuncs.rs Cairo Sierra libfunc glue code & implementations. ├─ metadata.rs Metadata injector to use within the compilation process. ├─ module.rs The MLIR module wrapper. - ├─ runtime.rs Methods that need to use the Rust runtime + ├─ runtime.rs Extern functions invoked from the compiled code. ├─ starknet_stub.rs ├─ starknet.rs Starknet syscall handler glue code. ├─ types.rs Conversion from Sierra to MLIR types. Cairo to MLIR type information. From 663b5c57164f363672fc6236b095040a39054ca2 Mon Sep 17 00:00:00 2001 From: DiegoC <90105443+DiegoCivi@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:29:19 -0300 Subject: [PATCH 27/35] Update docs/execution_walkthrough.md Co-authored-by: Julian Gonzalez Calderon --- docs/execution_walkthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index 286984eb97..3ec327c3eb 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -247,7 +247,7 @@ Sometimes we need to use stuff that would be too complicated or error-prone to implement in MLIR, but that we have readily available from Rust. When initializing an executor, for each of the variants of the `RuntimeBinding` enum, an MLIR global symbol -is declared that will contain a pointer to the runtime fucntion implementation. Then on execution, it will access the global symbol of the desired function and find its pointer. This way, each contract uses the +is declared that will contain a pointer to the runtime function implementation. Then on execution, it will access the global symbol of the desired function and find its pointer. This way, each contract uses the same runtime library located on the sequencer in a dynamic dispatch way. Although it's implemented in Rust, its functions use the C ABI and have Rust's From da4c7802262077846e3ce023d4dec69f62900cb2 Mon Sep 17 00:00:00 2001 From: DiegoC <90105443+DiegoCivi@users.noreply.github.com> Date: Fri, 18 Jul 2025 09:29:43 -0300 Subject: [PATCH 28/35] Update docs/execution_walkthrough.md Co-authored-by: Julian Gonzalez Calderon --- docs/execution_walkthrough.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/execution_walkthrough.md b/docs/execution_walkthrough.md index 3ec327c3eb..92cca3efa0 100644 --- a/docs/execution_walkthrough.md +++ b/docs/execution_walkthrough.md @@ -250,8 +250,8 @@ When initializing an executor, for each of the variants of the `RuntimeBinding` is declared that will contain a pointer to the runtime function implementation. Then on execution, it will access the global symbol of the desired function and find its pointer. This way, each contract uses the same runtime library located on the sequencer in a dynamic dispatch way. -Although it's implemented in Rust, its functions use the C ABI and have Rust's -name mangling disabled. This means that to the extern observer it's technically +Although it's implemented in Rust, its functions use the C ABI. +This means that to the extern observer it's technically indistinguishible from a library written in C. By doing this we're making the functions callable from MLIR. From a6858ab876eebb2cf166d5913756933bd5b5283e Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Tue, 22 Jul 2025 16:04:20 -0300 Subject: [PATCH 29/35] Refactor of starknet-native-compile description --- README.md | 5 ++--- src/bin/starknet-native-compile.rs | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5d4f754136..9c0edc7436 100644 --- a/README.md +++ b/README.md @@ -391,9 +391,8 @@ Options: ### `starknet-native-compile` ```bash -Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program from -felts into readable Sierra code, compiles it to native, and saves the result to the given output -path. +Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program from it into readable Sierra code, compiles it to a shared library, and saves the result to the given output path. Keep in mind, that when specifying the +output file path it should have a .so extension in Linux and a .dylib extension in Macos. Usage: starknet-native-compile [OPTIONS] diff --git a/src/bin/starknet-native-compile.rs b/src/bin/starknet-native-compile.rs index be9155c384..4cfc4b93d6 100644 --- a/src/bin/starknet-native-compile.rs +++ b/src/bin/starknet-native-compile.rs @@ -9,9 +9,10 @@ use cairo_lang_starknet_classes::contract_class::ContractClass; use cairo_native::executor::AotContractExecutor; use clap::Parser; -/// Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program from -/// felts into readable Sierra code, compiles it to native, and saves the result to the given output -/// path. +/// Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program +/// from it into readable Sierra code, compiles it to a shared library, and saves the result +/// to the given output path. Keep in mind, that when specifying the +/// output file path it should have a .so extension in Linux and a .dylib extension in Macos. #[derive(Parser, Debug)] #[clap(version, verbatim_doc_comment)] struct Args { From 567fc74d776530fae7a63035ea31829e7b915236 Mon Sep 17 00:00:00 2001 From: Diego Civini Date: Tue, 22 Jul 2025 16:07:37 -0300 Subject: [PATCH 30/35] cairo -> sierra --- docs/compilation_walkthrough.md | 2 +- src/bin/starknet-native-compile.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/compilation_walkthrough.md b/docs/compilation_walkthrough.md index a49f324c32..af83acc1cc 100644 --- a/docs/compilation_walkthrough.md +++ b/docs/compilation_walkthrough.md @@ -1,7 +1,7 @@ # Compilation walkthrough This section describes the entire process Cairo Native goes through to compile a -Cairo program to either a shared library (and how to use it) or a MLIR module +Sierra program to either a shared library (and how to use it) or a MLIR module for use in the JIT engine. ## General flow diff --git a/src/bin/starknet-native-compile.rs b/src/bin/starknet-native-compile.rs index 4cfc4b93d6..006773a254 100644 --- a/src/bin/starknet-native-compile.rs +++ b/src/bin/starknet-native-compile.rs @@ -11,8 +11,8 @@ use clap::Parser; /// Given a Sierra file (as saved in Starknet's contract tree), extracts the sierra_program /// from it into readable Sierra code, compiles it to a shared library, and saves the result -/// to the given output path. Keep in mind, that when specifying the -/// output file path it should have a .so extension in Linux and a .dylib extension in Macos. +/// to the given output path. Keep in mind, that when specifying the output file path it should +/// have a .so extension in Linux and a .dylib extension in Macos. #[derive(Parser, Debug)] #[clap(version, verbatim_doc_comment)] struct Args { From 753f850c061ce0e50bc0cd5290a3717a5f614aea Mon Sep 17 00:00:00 2001 From: DiegoC <90105443+DiegoCivi@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:09:51 -0300 Subject: [PATCH 31/35] Update docs/compilation_walkthrough.md Co-authored-by: Franco Giachetta --- docs/compilation_walkthrough.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/compilation_walkthrough.md b/docs/compilation_walkthrough.md index af83acc1cc..b24e2695ad 100644 --- a/docs/compilation_walkthrough.md +++ b/docs/compilation_walkthrough.md @@ -78,8 +78,8 @@ pub struct Program { The compilation process uses these fields through the `ProgramRegistry` to generate the relevant MLIR IR code. -To do all this we will need a MLIR context and a module created with said -context, the module describes a compilation unit, in this case, the Cairo +To do all this we will need a MLIR context and a MLIR module created it. The +module describes a compilation unit, in this case, the Cairo program. ## Initialization From 82022daca188aa33bd9c7286c4e618cad669b16d Mon Sep 17 00:00:00 2001 From: DiegoC <90105443+DiegoCivi@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:10:05 -0300 Subject: [PATCH 32/35] Update docs/compilation_walkthrough.md Co-authored-by: Franco Giachetta --- docs/compilation_walkthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compilation_walkthrough.md b/docs/compilation_walkthrough.md index b24e2695ad..558c104787 100644 --- a/docs/compilation_walkthrough.md +++ b/docs/compilation_walkthrough.md @@ -92,7 +92,7 @@ In Cairo Native we provide an API around context initialization, namely 2. Register all relevant MLIR dialects into the context. 3. Load the dialects. 4. Register all passes into the context. -5. Register all translations (ex. to LLVM IR) into the context. +5. Register all translations (e.g. to LLVM IR) into the context.