diff --git a/Makefile b/Makefile index 46cf3973a..3fe7ed6e8 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 6f50d3e79..977ccdf3d 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) @@ -46,6 +45,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 +54,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 +64,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 +127,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,13 +137,14 @@ 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. ```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. @@ -154,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. @@ -162,11 +166,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 +180,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 +202,7 @@ Options: ``` ### `cairo-native-dump` + ```bash Usage: cairo-native-dump [OPTIONS] @@ -210,10 +216,11 @@ 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. -Example: `echo '1' | cairo-native-run 'program.cairo' 'program::program::main' --inputs - --outputs -` +Example: `cairo-native-run --available-gas 10000 './programs/array_get.cairo'` ```bash Exits with 1 if the compilation or run fails, otherwise 0. @@ -234,6 +241,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. @@ -245,36 +253,66 @@ 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` + This tool runs a stress test on Cairo Native. ```bash @@ -315,6 +353,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 +362,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. @@ -339,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 @@ -348,6 +389,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/benches/compile_time.rs b/benches/compile_time.rs index d6caa5a7c..ec3b5a8e8 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 813898c23..9fda9bf2d 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 14633854e..000000000 --- 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 46a60705d..a49f324c3 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/docs/debugging.md b/docs/debugging.md index 88dc44f23..6ff4e828e 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 d5b78d26f..c0c107a9c 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,13 +138,13 @@ 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( 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). )?; @@ -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(); @@ -176,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). )?; @@ -197,34 +235,58 @@ 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 } ``` ## 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. -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 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 +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 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 +296,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. diff --git a/docs/gas_builtin_accounting.md b/docs/gas_builtin_accounting.md index 8f9cccb3e..ace202aab 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,17 +167,19 @@ 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 -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. +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. ### 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 aaef8d4d2..f5e5a5f83 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. diff --git a/docs/mlir.md b/docs/mlir.md index cf342235f..1835f7cfc 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 15409d79b..aa810adb6 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,23 @@ 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 - ├─ block_ext.rs A melior (MLIR) block trait extension to write less code. + ├─ bin/ Binary programs. + ├─ 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 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. @@ -118,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` @@ -144,10 +134,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. @@ -155,37 +145,39 @@ 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; -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 = &[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); + // 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).expect("entry point not found"); -// 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 @@ -199,8 +191,8 @@ 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::values::JitValue; +use cairo_native::executor::JitNativeExecutor; +use cairo_native::values::Value; use std::path::Path; fn main() { @@ -213,17 +205,17 @@ 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"))]; + 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); // 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 @@ -246,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}, @@ -275,7 +266,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(); @@ -286,14 +277,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(), + &[Felt::ONE], + Some(u64::MAX), + SyscallHandler::new() ) .expect("failed to execute the given contract");