diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ade377346e..fb92c4e12b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,6 +18,7 @@ env: cairo_programs/**/*.json !cairo_programs/manually_compiled/* cairo_programs/cairo-1-programs/bitwise.sierra + tests_cairo/**/*.json TEST_COLLECT_COVERAGE: 1 PROPTEST_CASES: 100 @@ -48,6 +49,7 @@ jobs: - cairo_test_programs - cairo_1_test_contracts - cairo_2_test_contracts + - tests_cairo_programs name: Build Cairo programs runs-on: ubuntu-24.04 steps: @@ -66,7 +68,7 @@ jobs: id: cache-programs with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} # This is not pretty, but we need `make` to see the compiled programs are # actually newer than the sources, otherwise it will try to rebuild them @@ -120,37 +122,43 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch proof programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch bench programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 1) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 2) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} + fail-on-cache-miss: true + - name: Fetch tests_cairo programs + uses: actions/cache/restore@v3 + with: + path: ${{ env.CAIRO_PROGRAMS_PATH }} + key: tests_cairo_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Merge caches uses: actions/cache/save@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} lint: needs: merge-caches @@ -179,7 +187,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run clippy @@ -229,7 +237,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # NOTE: we do this separately because --workspace operates in weird ways @@ -274,7 +282,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Check all features (workspace) @@ -309,7 +317,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Install testing tools @@ -390,7 +398,7 @@ jobs: cairo_programs/**/*.air_public_input cairo_programs/**/*.air_private_input cairo_programs/**/*.pie.zip - key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} - name: Install uv if: steps.trace-cache.outputs.cache-hit != 'true' @@ -409,7 +417,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # This is not pretty, but we need `make` to see the compiled programs are @@ -456,7 +464,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Generate traces @@ -615,7 +623,7 @@ jobs: cairo_programs/**/*.air_public_input cairo_programs/**/*.air_private_input cairo_programs/**/*.pie.zip - key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch traces for cairo-vm @@ -675,7 +683,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run script @@ -715,7 +723,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch pie diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5904a56f..59eb2c78e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ Both branches support Stwo prover opcodes (Blake2s, QM31) since v2.0.0. * fix: Fix off-by-one comparisons in `split_int`, `assert_250_bit`, and `sqrt` hints [#2348](https://github.com/lambdaclass/cairo-vm/pull/2348) +* chore: Add `CairoFunctionRunner` for running Cairo entrypoints by name or PC, and broaden `CairoArg`/`MaybeRelocatable` conversions to support primitive signed/unsigned integers and big integers [#2352](https://github.com/lambdaclass/cairo-vm/pull/2352) + +* chore: Add unit tests for `CairoFunctionRunner`, `CairoArg` conversions/macros, and `MaybeRelocatable` conversion macro coverage [#2354](https://github.com/lambdaclass/cairo-vm/pull/2354) +* feat: Add `CairoFunctionRunner` for running Cairo entrypoints by name or PC, and broaden `CairoArg`/`MaybeRelocatable` conversions to support primitive signed/unsigned integers and big integers [#2352](https://github.com/lambdaclass/cairo-vm/pull/2352) + +* test: Add `tests_cairo` utilities and a math Cairo test suite, including `assert_mr_eq!` and structured error helpers [#2353](https://github.com/lambdaclass/cairo-vm/pull/2353) + #### [3.2.0] - 2026-3-3 * fix: Change extended_resource_counter entry from u32 to usize [#2349](https://github.com/lambdaclass/cairo-vm/pull/2349) diff --git a/Makefile b/Makefile index 1734325614..5fd8556faa 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ UNAME := $(shell uname) hyper-threading-benchmarks \ cairo_bench_programs cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts \ cairo_trace cairo-vm_trace cairo_proof_trace cairo-vm_proof_trace python-deps python-deps-macos \ - build-cairo-lang hint-accountant \ create-proof-programs-symlinks \ + build-cairo-lang hint-accountant \ create-proof-programs-symlinks tests_cairo_programs \ $(RELBIN) $(DBGBIN) # Proof mode consumes too much memory with cairo-lang to execute @@ -268,6 +268,21 @@ run: check: cargo check +# ====================== +# Tests Cairo Programs +# ====================== + +TESTS_CAIRO_DIR=tests_cairo +TESTS_CAIRO_FILES:=$(shell find $(TESTS_CAIRO_DIR) -name "*.cairo") +COMPILED_TESTS_CAIRO:=$(patsubst %.cairo, %.json, $(TESTS_CAIRO_FILES)) + +$(TESTS_CAIRO_DIR)/%.json: $(TESTS_CAIRO_DIR)/%.cairo + . cairo-vm-env/bin/activate && cairo-compile \ + --cairo_path="$(TESTS_CAIRO_DIR)" \ + $< --output $@ + +tests_cairo_programs: $(COMPILED_TESTS_CAIRO) + cairo_test_programs: $(COMPILED_TESTS) $(COMPILED_BAD_TESTS) $(COMPILED_NORETROCOMPAT_TESTS) $(COMPILED_PRINT_TESTS) $(COMPILED_MOD_BUILTIN_TESTS) $(COMPILED_SECP_CAIRO0_HINTS) $(COMPILED_KZG_DA_CAIRO0_HINTS) $(COMPILED_SEGMENT_ARENA_CAIRO0_HINTS) cairo_proof_programs: $(COMPILED_PROOF_TESTS) $(COMPILED_MOD_BUILTIN_PROOF_TESTS) $(COMPILED_STWO_EXCLUSIVE_TESTS) cairo_bench_programs: $(COMPILED_BENCHES) @@ -286,7 +301,7 @@ ifdef TEST_COLLECT_COVERAGE TEST_COMMAND:=cargo llvm-cov nextest --no-report endif -test: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program +test: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program tests_cairo_programs $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints" test-extensive_hints: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_1_program cairo_2_test_contracts $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints, cairo-0-secp-hints, cairo-0-data-availability-hints, extensive_hints" diff --git a/README.md b/README.md index 1d7dbe9764..bda09bbedb 100644 --- a/README.md +++ b/README.md @@ -246,19 +246,6 @@ When running a Cairo program directly using the Cairo-vm repository you would fi cairo_runner.initialize_segments(None); ``` -When using cairo-vm with the Starknet devnet there are additional parameters that are part of the OS context passed on to the `run_from_entrypoint` method that we do not have here when using it directly. These parameters are, for example, initial stacks of the builtins, which are the base of each of them and are needed as they are the implicit arguments of the function. - -```rust - let _var = cairo_runner.run_from_entrypoint( - entrypoint, - vec![ - &MaybeRelocatable::from(2).into(), //this is the entry point selector - &MaybeRelocatable::from((2,0)).into() //this would be the output_ptr for example if our cairo function uses it - ], - false, - &mut hint_processor, - ); -``` ### Running cairo 1 programs To run a cairo 1 program enter in the folder `cd cairo1-run` and follow the [`cairo1-run documentation`](cairo1-run/README.md) diff --git a/tests_cairo/error_utils.rs b/tests_cairo/error_utils.rs new file mode 100644 index 0000000000..0851b0d34d --- /dev/null +++ b/tests_cairo/error_utils.rs @@ -0,0 +1,170 @@ +//! Test utilities for Cairo VM result assertions. + +use cairo_vm::vm::errors::{ + cairo_run_errors::CairoRunError, hint_errors::HintError, vm_errors::VirtualMachineError, + vm_exception::VmException, +}; + +/// Asserts VM result is `Ok` or matches an error pattern. +#[macro_export] +macro_rules! assert_vm_result { + ($res:expr, ok $(,)?) => {{ + match &$res { + Ok(_) => {} + Err(e) => panic!("Expected Ok, got Err: {:#?}", e), + } + }}; + + ($res:expr, err $pat:pat $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat), + "Unexpected error variant.\nExpected: {}\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; + + ($res:expr, err $pat:pat if $guard:expr $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat if $guard), + "Unexpected error variant.\nExpected: {} (with guard)\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; +} + +/// Type alias for check functions that validate test results. +pub type VmCheck = fn(&std::result::Result); + +/// Asserts that the result is `Ok`. +pub fn expect_ok(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!(res, ok); +} + +/// Asserts that the result is `HintError::AssertNotZero`. +pub fn expect_hint_assert_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotZero(_))) + ); +} + +/// Asserts that the result is `HintError::AssertNotEqualFail`. +pub fn expect_assert_not_equal_fail(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotEqualFail(_))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffTypeComparison)`. +pub fn expect_diff_type_comparison(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffTypeComparison(_)))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffIndexComp)`. +pub fn expect_diff_index_comp(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffIndexComp(_)))) + ); +} + +/// Asserts that the result is `HintError::ValueOutside250BitRange`. +pub fn expect_hint_value_outside_250_bit_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutside250BitRange(_))) + ); +} + +/// Asserts that the result is `HintError::NonLeFelt252`. +pub fn expect_non_le_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::NonLeFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::AssertLtFelt252`. +pub fn expect_assert_lt_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertLtFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::ValueOutsideValidRange`. +pub fn expect_hint_value_outside_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutsideValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::OutOfValidRange`. +pub fn expect_hint_out_of_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::OutOfValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::SplitIntNotZero`. +pub fn expect_split_int_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntNotZero)) + ); +} + +/// Asserts that the result is `HintError::SplitIntLimbOutOfRange`. +pub fn expect_split_int_limb_out_of_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntLimbOutOfRange(_))) + ); +} diff --git a/tests_cairo/math/main_math_test.cairo b/tests_cairo/math/main_math_test.cairo new file mode 100644 index 0000000000..c8979c82be --- /dev/null +++ b/tests_cairo/math/main_math_test.cairo @@ -0,0 +1,28 @@ +from starkware.cairo.common.math import ( + assert_not_zero, + assert_not_equal, + assert_nn, + assert_le, + assert_lt, + assert_nn_le, + assert_in_range, + assert_250_bit, + split_felt, + assert_le_felt, + assert_lt_felt, + abs_value, + sign, + unsigned_div_rem, + signed_div_rem, + split_int, + sqrt, + horner_eval, + is_quad_residue, + safe_div, + safe_mult, + assert_is_power_of_2, +) + +func main() { +return (); +} diff --git a/tests_cairo/math/math_test.rs b/tests_cairo/math/math_test.rs new file mode 100644 index 0000000000..06a6a2b944 --- /dev/null +++ b/tests_cairo/math/math_test.rs @@ -0,0 +1,991 @@ +//! Tests for `math.cairo`. + +use std::sync::LazyLock; + +use super::math_test_utils::{is_quad_residue_mod_prime, MAX_DIV, RC_BOUND}; +use crate::error_utils::{ + expect_assert_lt_felt252, expect_assert_not_equal_fail, expect_diff_index_comp, + expect_diff_type_comparison, expect_hint_assert_not_zero, expect_hint_out_of_valid_range, + expect_hint_value_outside_250_bit_range, expect_hint_value_outside_valid_range, + expect_non_le_felt252, expect_ok, expect_split_int_limb_out_of_range, + expect_split_int_not_zero, VmCheck, +}; +use cairo_vm::cairo_args; +use cairo_vm::types::builtin_name::BuiltinName; +use cairo_vm::types::program::Program; +use cairo_vm::types::relocatable::MaybeRelocatable; +use cairo_vm::utils::CAIRO_PRIME; +use cairo_vm::vm::runners::cairo_function_runner::CairoFunctionRunner; +use cairo_vm::Felt252; +use num_bigint::{BigInt, BigUint, RandBigInt}; +use num_traits::{One, Signed, Zero}; +use rand::thread_rng; +use rstest::{fixture, rstest}; + +// ===================== Shared constants (LazyLock) ===================== + +/// The compiled Cairo math program, loaded once and shared across all tests. +static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); + +/// Interesting felt values used in several tests. +static INTERESTING_FELTS: LazyLock> = LazyLock::new(|| { + let p = &*CAIRO_PRIME; + vec![ + BigUint::from(0u64), + BigUint::from(1u64), + BigUint::from(2u64).pow(128) - BigUint::one(), + BigUint::from(2u64).pow(128), + BigUint::from(2u64).pow(128) + BigUint::one(), + p / BigUint::from(3u64) - BigUint::one(), + p / BigUint::from(3u64), + p / BigUint::from(3u64) + BigUint::one(), + p / BigUint::from(2u64) - BigUint::one(), + p / BigUint::from(2u64), + p / BigUint::from(2u64) + BigUint::one(), + BigUint::from(2u64).pow(251) - BigUint::one(), + BigUint::from(2u64).pow(251), + BigUint::from(2u64).pow(251) + BigUint::one(), + p - BigUint::from(2u64), + p - BigUint::one(), + ] +}); + +// ===================== Helpers ===================== + +// ===================== Fixture ===================== + +/// Creates a fresh CairoFunctionRunner from the shared PROGRAM. +#[fixture] +fn runner() -> CairoFunctionRunner { + CairoFunctionRunner::new(&PROGRAM).unwrap() +} + +// ===================== test_assert_not_zero ===================== + +#[rstest] +// Case: value=7 +// Expected: Success. +#[case(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=0 +// Expected: Error. +#[case(Some(BigUint::zero()), expect_hint_assert_not_zero)] +fn test_assert_not_zero(#[case] value: Option, #[case] check: VmCheck<()>) { + let value = match value { + Some(v) => v, + None => { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), &CAIRO_PRIME) + } + }; + + let mut runner = runner(); + let args = cairo_args!(value); + let res = runner.run_default_cairo0("assert_not_zero", &args); + check(&res); +} + +// ===================== test_assert_not_equal ===================== + +#[rstest] +// Not equal integers +// Case: a=3, b=7 +// Expected: Success. +#[case::not_equal_ints(MaybeRelocatable::from(3), MaybeRelocatable::from(7), expect_ok)] +// Not equal relocatables (same segment, different offset) +// Case: a=(2, 5), b=(2, 10) +// Expected: Success. +#[case::not_equal_relocs( + MaybeRelocatable::from((2isize, 5)), + MaybeRelocatable::from((2isize, 10)), + expect_ok +)] +// Equal integers +// Case: a=5, b=5 +// Expected: Error. +#[case::equal_ints( + MaybeRelocatable::from(5), + MaybeRelocatable::from(5), + expect_assert_not_equal_fail +)] +// Equal relocatables +// Case: a=(1, 5), b=(1, 5) +// Expected: Error. +#[case::equal_relocs( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from((1isize, 5)), + expect_assert_not_equal_fail +)] +// Non-comparable: relocatable vs int +// Case: a=(1, 5), b=0 +// Expected: Error. +#[case::non_comparable_reloc_vs_int( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from(0), + expect_diff_type_comparison +)] +// Non-comparable: different segments +// Case: a=(1, 5), b=(2, 3) +// Expected: Error. +#[case::non_comparable_diff_segments( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from((2isize, 3)), + expect_diff_index_comp +)] + +fn test_assert_not_equal( + #[case] a: MaybeRelocatable, + #[case] b: MaybeRelocatable, + #[case] check: VmCheck<()>, +) { + let mut runner = runner(); + let args = cairo_args!(a, b); + let res = runner.run_default_cairo0("assert_not_equal", &args); + check(&res); +} + +// ===================== test_assert_250_bit ===================== +#[rstest] +// Valid cases (should pass) +// Case: value=0 +// Expected: Success. +#[case::zero(BigUint::from(0u64), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(BigUint::from(1u64), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(BigUint::from(2u64).pow(250) - BigUint::one(), expect_ok)] +// Invalid cases (should fail) +// Case: value=2^250 +// Expected: Error. +#[case::at_boundary(BigUint::from(2u64).pow(250), expect_hint_value_outside_250_bit_range)] +// Case: value=(2^250)+1 +// Expected: Error. +#[case::above_boundary(BigUint::from(2u64).pow(250) + BigUint::one(), expect_hint_value_outside_250_bit_range)] +// Case: value=2^251 +// Expected: Error. +#[case::way_above(BigUint::from(2u64).pow(251), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::near_prime(&*CAIRO_PRIME - BigUint::one(), expect_hint_value_outside_250_bit_range)] +fn test_assert_250_bit( + mut runner: CairoFunctionRunner, + #[case] value: BigUint, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value); + let res = runner.run_default_cairo0("assert_250_bit", &args); + check(&res); + + // If successful, verify the return value + if res.is_ok() { + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(3usize).unwrap()); + } +} + +// ===================== test_split_felt ===================== + +#[rstest] +// Case: idx=0 +// Expected: Success. +#[case::idx_0(0)] +// Case: idx=1 +// Expected: Success. +#[case::idx_1(1)] +// Case: idx=2 +// Expected: Success. +#[case::idx_2(2)] +// Case: idx=3 +// Expected: Success. +#[case::idx_3(3)] +// Case: idx=4 +// Expected: Success. +#[case::idx_4(4)] +// Case: idx=5 +// Expected: Success. +#[case::idx_5(5)] +// Case: idx=6 +// Expected: Success. +#[case::idx_6(6)] +// Case: idx=7 +// Expected: Success. +#[case::idx_7(7)] +// Case: idx=8 +// Expected: Success. +#[case::idx_8(8)] +// Case: idx=9 +// Expected: Success. +#[case::idx_9(9)] +// Case: idx=10 +// Expected: Success. +#[case::idx_10(10)] +// Case: idx=11 +// Expected: Success. +#[case::idx_11(11)] +// Case: idx=12 +// Expected: Success. +#[case::idx_12(12)] +// Case: idx=13 +// Expected: Success. +#[case::idx_13(13)] +// Case: idx=14 +// Expected: Success. +#[case::idx_14(14)] +// Case: idx=15 +// Expected: Success. +#[case::idx_15(15)] +fn test_split_felt(mut runner: CairoFunctionRunner, #[case] idx: usize) { + let mask_128 = BigUint::from(2u64).pow(128) - BigUint::one(); + let value = &INTERESTING_FELTS[idx]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let expected_high: BigUint = value >> 128; + let expected_low = value & &mask_128; + + let args = cairo_args!(rc_base.clone(), value); + runner + .run_default_cairo0("split_felt", &args) + .unwrap_or_else(|e| panic!("split_felt failed for value {value}: {e}")); + + let ret = runner.get_return_values(3).unwrap(); + // ret = [range_check_ptr, high, low] + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(3usize).unwrap(), + "range_check_ptr mismatch for value {value}" + ); + assert_mr_eq!(&ret[1], &expected_high, "high mismatch for value {value}"); + assert_mr_eq!(&ret[2], &expected_low, "low mismatch for value {value}"); +} + +// ===================== test_assert_le_felt ===================== + +#[rstest] +fn test_assert_le_felt( + mut runner: CairoFunctionRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 <= value1 { + runner + .run_default_cairo0("assert_le_felt", &args) + .unwrap_or_else(|e| panic!("assert_le_felt failed for {value0} <= {value1}: {e}")); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for {value0} <= {value1}" + ); + } else { + let result = runner.run_default_cairo0("assert_le_felt", &args); + expect_non_le_felt252(&result); + } +} + +// ===================== test_assert_lt_felt ===================== + +#[rstest] +fn test_assert_lt_felt( + mut runner: CairoFunctionRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 < value1 { + runner + .run_default_cairo0("assert_lt_felt", &args) + .unwrap_or_else(|e| panic!("assert_lt_felt failed for {value0} < {value1}: {e}")); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for {value0} < {value1}" + ); + } else { + let result = runner.run_default_cairo0("assert_lt_felt", &args); + expect_assert_lt_felt252(&result); + } +} + +// ===================== test_abs_value ===================== + +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_abs_value( + mut runner: CairoFunctionRunner, + #[case] value_case: BigInt, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("abs_value", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(1usize).unwrap()); + assert_mr_eq!(&ret[1], abs_value); + } +} + +// ===================== test_sign ===================== +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_sign( + mut runner: CairoFunctionRunner, + #[case] value_case: BigInt, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("sign", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.get_return_values(2).unwrap(); + // range_check_ptr == rc_base + (1 if value != 0 else 0) + let expected_rc_ptr = if value_case.is_zero() { + rc_base + } else { + rc_base.add_usize(1usize).unwrap() + }; + assert_mr_eq!(&ret[0], &expected_rc_ptr); + + // res == (0 if value == 0 else 1 if value > 0 else PRIME - 1) + let expected_sign = if value_case.is_zero() { + BigUint::zero() + } else if value_case.is_positive() { + BigUint::one() + } else { + &*CAIRO_PRIME - BigUint::one() + }; + assert_mr_eq!(&ret[1], &expected_sign); + } +} + +// ===================== test_unsigned_div_rem ===================== + +#[rstest] +// 1) q=1333, div=17, r=3 +// Case: q=1333, div=17, r=3 +// Expected: Success. +#[case::case_1_basic( + Some(BigUint::from(1333u64)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + expect_ok +)] +// 2) q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_2_max_values( + Some(&*RC_BOUND - BigUint::one()), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 3) q=random, div=MAX_DIV, r=0 +// Case: q=random, div=MAX_DIV, r=0 +// Expected: Success. +#[case::case_3_random_q( + None, + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + expect_ok +)] +// 4) q=random, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_4_random_q( + None, + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 5) q=random, div=MAX_DIV, r=random +// Case: q=random, div=MAX_DIV, r=random +// Expected: Success. +#[case::case_5_random_q_and_r( + None, + Some(MAX_DIV.clone()), + None, + expect_ok +)] +// 6) q=random, div=random, r=random +// Case: q=random, div=random, r=random +// Expected: Success. +#[case::case_6_all_random(None, None, None, expect_ok)] +// 7) q=1, div=MAX_DIV+1, r=random -> expected error. +// Case: q=1, div=MAX_DIV+1, r=random +// Expected: Error. +#[case::case_7_invalid_div( + Some(BigUint::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + expect_hint_out_of_valid_range +)] +fn test_unsigned_div_rem( + mut runner: CairoFunctionRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + // Verify rc_bound matches expected RC_BOUND (2^128) + let rc_bound = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + assert_eq!(rc_bound, *RC_BOUND, "Unexpected rc_bound"); + + let mut rng = thread_rng(); + // Python uses div in [0, MAX_DIV], but remainder generation requires div > 0. + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &RC_BOUND), + }; + + let value = &q * &div + &r; + + // Assert value < PRIME (as in Python test) + assert!( + value < *CAIRO_PRIME, + "Generated value is too large. q={q}, div={div}, r={r}" + ); + + let args = cairo_args!(rc_base.clone(), value, div); + let result = runner.run_default_cairo0("unsigned_div_rem", &args); + check(&result); + + // If successful, verify the results match expected values + if result.is_ok() { + let ret = runner.get_return_values(3).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(3usize).unwrap(), + "range_check_ptr mismatch" + ); + assert_mr_eq!(&ret[1], &q, "quotient mismatch"); + assert_mr_eq!(&ret[2], &r, "remainder mismatch"); + } +} + +// ===================== test_signed_div_rem ===================== +#[rstest] +// Case: q=1333, div=17, r=3, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::basic( + Some(BigInt::from(1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=-1333, div=17, r=3, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::negative_basic( + Some(BigInt::from(-1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=RC_BOUND/2-1, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::max_pos( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + None, + expect_ok +)] +// Case: q=-(RC_BOUND/2)+1, div=MAX_DIV, r=0, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::max_neg( + Some(-(BigInt::from(&*RC_BOUND / BigUint::from(2u64))) + BigInt::one()), + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + None, + expect_ok +)] +// Case: q=random, div=MAX_DIV, r=0, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_zero(None, Some(MAX_DIV.clone()), Some(BigUint::zero()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_max(None, Some(MAX_DIV.clone()), Some(&*MAX_DIV - BigUint::one()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=random, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_random_r(None, Some(MAX_DIV.clone()), None, None, expect_ok)] +// Case: q=RC_BOUND/2-1, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_pos_q( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=-RC_BOUND/2, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_neg_q( + Some(-BigInt::from(&*RC_BOUND / BigUint::from(2u64))), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=1, div=MAX_DIV+1, r=random, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Error. +#[case::invalid_div( + Some(BigInt::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + None, + expect_hint_out_of_valid_range +)] +// Case: q=random, div=random, r=random, bound=RC_BOUND/2+1 +// Expected: Error. +#[case::invalid_bound( + None, + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64) + BigUint::one()), + expect_hint_out_of_valid_range +)] +fn test_signed_div_rem( + mut runner: CairoFunctionRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] bound: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let half_rc_bound = &*RC_BOUND / BigUint::from(2u64); + let mut rng = thread_rng(); + + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => { + let min = -BigInt::from(half_rc_bound.clone()); + let max = BigInt::from(half_rc_bound.clone()); + rng.gen_bigint_range(&min, &max) + } + }; + let bound = match bound { + Some(v) => v, + None => { + let lower = if q >= BigInt::zero() { + q.clone() + BigInt::one() + } else { + -q.clone() + }; + let upper = BigInt::from(half_rc_bound.clone() + BigUint::one()); + rng.gen_bigint_range(&lower, &upper) + .to_biguint() + .expect("bound should be non-negative") + } + }; + + let value = q.clone() * BigInt::from(div.clone()) + BigInt::from(r.clone()); + let half_prime = BigInt::from((&*CAIRO_PRIME) >> 1usize); + let neg_half_prime = -half_prime.clone(); + assert!( + value >= neg_half_prime && value < half_prime, + "Generated value is too large." + ); + + let args = cairo_args!(rc_base.clone(), value, div, bound); + let result = runner.run_default_cairo0("signed_div_rem", &args); + check(&result); + + if result.is_ok() { + let ret = runner.get_return_values(3).unwrap(); + let rc_ptr = &ret[0]; + let result_q = &ret[1]; + let result_r = &ret[2]; + + assert_mr_eq!(rc_ptr, &rc_base.add_usize(4usize).unwrap()); + // Expected_q = q % PRIME (field element conversion). + let expected_q = Felt252::from(&q); + assert_mr_eq!(result_q, &expected_q); + assert_mr_eq!(result_r, &r); + } +} + +// ===================== test_split_int ===================== +#[rstest] +// Case: value=0x1234FCDA, n=10, base=16, bound=16, expected_output=vec![0xA, 0xD, +// 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0] Expected: Success. +#[case::hex_digits( + 0x1234FCDA_i64, + 10_i64, + 16_i64, + 16_i64, + Some(vec![0xA, 0xD, 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=256, bound=256, expected_output=vec![0xDA, +// 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0] Expected: Success. +#[case::byte_pairs( + 0x1234FCDA_i64, + 10_i64, + 256_i64, + 256_i64, + Some(vec![0xDA, 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=16, bound=15, expected_output=random +// Expected: Error. +#[case::out_of_bound_limb( + 0x1234FCDA_i64, + 10_i64, + 16_i64, + 15_i64, + None, + expect_split_int_limb_out_of_range +)] +// Case: value=0xAAA, n=3, base=16, bound=11, expected_output=vec![0xA, 0xA, 0xA] +// Expected: Success. +#[case::exact_fit( + 0xAAA_i64, + 3_i64, + 16_i64, + 11_i64, + Some(vec![0xA, 0xA, 0xA]), + expect_ok +)] +// Case: value=0xAAA, n=3, base=16, bound=10, expected_output=random +// Expected: Error. +#[case::bound_too_small( + 0xAAA_i64, + 3_i64, + 16_i64, + 10_i64, + None, + expect_split_int_limb_out_of_range +)] +// Case: value=0xAAA, n=2, base=16, bound=16, expected_output=random +// Expected: Error. +#[case::value_out_of_range(0xAAA_i64, 2_i64, 16_i64, 16_i64, None, expect_split_int_not_zero)] +fn test_split_int( + mut runner: CairoFunctionRunner, + #[case] value: i64, + #[case] n: i64, + #[case] base: i64, + #[case] bound: i64, + #[case] expected_output: Option>, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let output = runner.runner.vm.add_memory_segment(); + let output_mr = MaybeRelocatable::from(output); + + let args = cairo_args!(rc_base.clone(), value, n, base, bound, output_mr); + let result = runner.run_default_cairo0("split_int", &args); + check(&result); + + if result.is_ok() { + let expected_output = + expected_output.expect("expected_output must be set for success case"); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(2usize * n as usize).unwrap()); + + let range = runner.runner.vm.get_range(output, n as usize); + assert_eq!( + range.len(), + expected_output.len(), + "split_int output length mismatch" + ); + for (i, (actual, exp)) in range.iter().zip(expected_output.iter()).enumerate() { + let actual_val = actual + .as_ref() + .unwrap_or_else(|| panic!("Missing output at index {i}")); + assert_mr_eq!( + actual_val.as_ref(), + *exp, + "split_int output mismatch at index {i}" + ); + } + } +} +// ===================== test_sqrt ===================== + +#[rstest] +// Case: value=0 +// Expected: Success. +#[case::zero(Some(BigUint::from(0u64)), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(Some(BigUint::from(1u64)), expect_ok)] +// Case: value=2 +// Expected: Success. +#[case::two(Some(BigUint::from(2u64)), expect_ok)] +// Case: value=3 +// Expected: Success. +#[case::three(Some(BigUint::from(3u64)), expect_ok)] +// Case: value=4 +// Expected: Success. +#[case::four(Some(BigUint::from(4u64)), expect_ok)] +// Case: value=5 +// Expected: Success. +#[case::five(Some(BigUint::from(5u64)), expect_ok)] +// Case: value=6 +// Expected: Success. +#[case::six(Some(BigUint::from(6u64)), expect_ok)] +// Case: value=7 +// Expected: Success. +#[case::seven(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=8 +// Expected: Success. +#[case::eight(Some(BigUint::from(8u64)), expect_ok)] +// Case: value=9 +// Expected: Success. +#[case::nine(Some(BigUint::from(9u64)), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(Some(BigUint::from(2u64).pow(250) - BigUint::one()), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=2^250 +// Expected: Error. +#[case::out_of_range_2_pow_250(Some(BigUint::from(2u64).pow(250)), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::out_of_range_prime_minus_one( + Some(&*CAIRO_PRIME - BigUint::one()), + expect_hint_value_outside_250_bit_range +)] +fn test_sqrt( + mut runner: CairoFunctionRunner, + #[case] value: Option, + #[case] check: VmCheck<()>, +) { + let value = value.unwrap_or_else(|| { + let mut rng = thread_rng(); + let upper = BigUint::one() << 250usize; + rng.gen_biguint_range(&BigUint::zero(), &upper) + }); + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value.clone()); + let result = runner.run_default_cairo0("sqrt", &args); + check(&result); + + if result.is_ok() { + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for sqrt({value})" + ); + + let expected_root = value.sqrt(); + assert_mr_eq!( + &ret[1], + &expected_root, + "sqrt result mismatch for value={value}" + ); + } +} + +// ===================== test_horner_eval ===================== + +#[rstest] +// Case: n=0 +// Expected: Success. +#[case::zero_coefficients(0)] +// Case: n=16 +// Expected: Success. +#[case::sixteen_coefficients(16)] +fn test_horner_eval(mut runner: CairoFunctionRunner, #[case] n: usize) { + let mut rng = thread_rng(); + let prime = &*CAIRO_PRIME; + + // Generate random coefficients in [0, PRIME) + let coefficients: Vec = (0..n) + .map(|_| rng.gen_biguint_range(&BigUint::zero(), prime)) + .collect(); + let coeff_mr: Vec = coefficients.iter().map(MaybeRelocatable::from).collect(); + + // Generate random point in [0, PRIME) + let point = rng.gen_biguint_range(&BigUint::zero(), prime); + + // horner_eval takes (n, coefficients_ptr, point) - coefficients is an array + let args = cairo_args!(n, coeff_mr, point.clone()); + runner.run_default_cairo0("horner_eval", &args).unwrap(); + + let ret = runner.get_return_values(1).unwrap(); + + // Compute expected result: sum(coef * point^i for i, coef in enumerate(coefficients)) % PRIME + let expected: BigUint = coefficients + .iter() + .enumerate() + .map(|(i, coef)| coef * point.modpow(&BigUint::from(i), prime)) + .fold(BigUint::zero(), |acc, x| (acc + x) % prime); + + assert_mr_eq!(&ret[0], &expected); +} + +// ===================== test_is_quad_residue ===================== + +#[rstest] +// Case: x=0 +// Expected: Success. +#[case::zero(Some(BigUint::zero()))] +// Case: x=random +// Expected: Success. +#[case::random(None)] +fn test_is_quad_residue(mut runner: CairoFunctionRunner, #[case] x: Option) { + let prime = &*CAIRO_PRIME; + + let x = x.unwrap_or_else(|| { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), prime) + }); + + // Test is_quad_residue(x) + let args = cairo_args!(x.clone()); + runner.run_default_cairo0("is_quad_residue", &args).unwrap(); + let ret = runner.get_return_values(1).unwrap(); + + let expected = is_quad_residue_mod_prime(&x); + assert_mr_eq!( + &ret[0], + expected, + "is_quad_residue({x}) should return {expected}" + ); + + // Test is_quad_residue(3 * x) + // 3 is not a quadratic residue modulo PRIME + let mut runner2 = CairoFunctionRunner::new(&PROGRAM).unwrap(); + let three_x = (BigUint::from(3u64) * &x) % prime; + let args2 = cairo_args!(three_x); + runner2 + .run_default_cairo0("is_quad_residue", &args2) + .unwrap(); + let ret2 = runner2.get_return_values(1).unwrap(); + + let expected2 = if x.is_zero() { + 1i64 // 3 * 0 = 0, which is QR + } else if is_quad_residue_mod_prime(&x) == 1 { + 0i64 // x is QR, 3 is not QR, so 3*x is not QR + } else { + 1i64 // x is not QR, 3 is not QR, so 3*x is QR (product of two non-QR is QR) + }; + assert_mr_eq!( + &ret2[0], + expected2, + "is_quad_residue(3 * {x}) should return {expected2}" + ); +} diff --git a/tests_cairo/math/math_test_utils.rs b/tests_cairo/math/math_test_utils.rs new file mode 100644 index 0000000000..1fb7d0a884 --- /dev/null +++ b/tests_cairo/math/math_test_utils.rs @@ -0,0 +1,19 @@ +use std::sync::LazyLock; + +use cairo_vm::{math_utils::is_quad_residue, utils::CAIRO_PRIME}; +use num_bigint::BigUint; +use num_integer::Integer; +/// RC_BOUND = 2^128 +pub static RC_BOUND: LazyLock = LazyLock::new(|| BigUint::from(2u64).pow(128)); + +/// MAX_DIV = CAIRO_PRIME // RC_BOUND +pub static MAX_DIV: LazyLock = LazyLock::new(|| CAIRO_PRIME.div_floor(&RC_BOUND)); + +/// Returns 1 if `a` is a quadratic residue modulo CAIRO_PRIME, 0 if not, and -1 on error. +pub fn is_quad_residue_mod_prime(a: &BigUint) -> i64 { + match is_quad_residue(a, &CAIRO_PRIME) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } +} diff --git a/tests_cairo/math/mod.rs b/tests_cairo/math/mod.rs new file mode 100644 index 0000000000..668a4913e5 --- /dev/null +++ b/tests_cairo/math/mod.rs @@ -0,0 +1,2 @@ +mod math_test; +mod math_test_utils; diff --git a/tests_cairo/mod.rs b/tests_cairo/mod.rs new file mode 100644 index 0000000000..8db59cc439 --- /dev/null +++ b/tests_cairo/mod.rs @@ -0,0 +1,6 @@ +#![cfg(test)] + +#[macro_use] +mod test_utils; +mod error_utils; +mod math; diff --git a/tests_cairo/test_utils.rs b/tests_cairo/test_utils.rs new file mode 100644 index 0000000000..e8a529ac66 --- /dev/null +++ b/tests_cairo/test_utils.rs @@ -0,0 +1,56 @@ +/// Loads a compiled Cairo `.json` program from the same directory as the calling source file. +/// +/// Pass only the filename (no directory prefix). The directory is inferred from the call site +/// via `file!()`, so the `.json` must live next to the `.cairo` and `.rs` files. +/// +/// # Example +/// ```rust +/// static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); +/// ``` +/// +/// # Panics +/// - If the `.json` file does not exist: run `make tests_cairo_programs` first. +/// - If the `.json` file cannot be parsed as a Cairo `Program`. +#[macro_export] +macro_rules! load_cairo_program { + ($name:literal) => {{ + // file!() expands at the call site, giving the path of the calling source file + // relative to the workspace root (e.g. "tests_cairo/math/math_test.rs"). + // We derive the directory from it and join with the requested filename. + let source_dir = std::path::Path::new(file!()) + .parent() + .expect("source file should have a parent directory"); + // CARGO_MANIFEST_DIR is the `vm/` crate dir; workspace root is one level up. + let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("vm crate should have a parent directory"); + let json_path = workspace_root.join(source_dir).join($name); + + let bytes = std::fs::read(&json_path).unwrap_or_else(|err| { + panic!( + "Cairo program not found at {json_path:?}: {err}\n\ + Did you run `make tests_cairo_programs`?" + ) + }); + + cairo_vm::types::program::Program::from_bytes(&bytes, None) + .unwrap_or_else(|e| panic!("Failed to parse Cairo program at {json_path:?}: {e}")) + }}; +} + +/// Asserts that a `MaybeRelocatable` reference equals a value convertible into `MaybeRelocatable`. +#[macro_export] +macro_rules! assert_mr_eq { + ($left:expr, $right:expr) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr); + }}; + ($left:expr, $right:expr, $($arg:tt)+) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr, $($arg)+); + }}; +} diff --git a/vm/Cargo.toml b/vm/Cargo.toml index 03ec972fb9..2db9687648 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -21,7 +21,11 @@ cairo-0-secp-hints = [] cairo-0-data-availability-hints = [] # Note that these features are not retro-compatible with the cairo Python VM. -test_utils = ["dep:arbitrary", "starknet-types-core/arbitrary", "starknet-types-core/std"] # This feature will reference every test-oriented feature +test_utils = [ + "dep:arbitrary", + "starknet-types-core/arbitrary", + "starknet-types-core/std", +] # This feature will reference every test-oriented feature # Allows extending the set of hints for the current vm run from within a hint. # For a usage example checkout vm/src/tests/run_deprecated_contract_class_simplified.rs extensive_hints = [] @@ -45,7 +49,12 @@ generic-array = { workspace = true } keccak = { workspace = true } anyhow = { workspace = true } thiserror = { workspace = true } -starknet-types-core = { version = "0.2.4", default-features = false, features = ["serde", "curve", "num-traits", "hash"] } +starknet-types-core = { version = "0.2.4", default-features = false, features = [ + "serde", + "curve", + "num-traits", + "hash", +] } rust_decimal = { version = "1.35.0", default-features = false } num-prime = { version = "0.4.3", features = ["big-int"] } @@ -61,7 +70,7 @@ ark-std = { workspace = true, optional = true } arbitrary = { workspace = true, features = ["derive"], optional = true } # Used to derive clap traits for CLIs -clap = { version = "4.3.10", features = ["derive"], optional = true} +clap = { version = "4.3.10", features = ["derive"], optional = true } [dev-dependencies] assert_matches = "1.5.0" @@ -81,6 +90,10 @@ path = "../bench/criterion_benchmark.rs" name = "criterion_benchmark" harness = false +[[test]] +name = "math_test" +path = "../tests_cairo/mod.rs" + [[example]] name = "custom_hint" path = "../examples/custom_hint/src/main.rs" diff --git a/vm/src/math_utils/mod.rs b/vm/src/math_utils/mod.rs index aa77df4219..f150f4c900 100644 --- a/vm/src/math_utils/mod.rs +++ b/vm/src/math_utils/mod.rs @@ -397,7 +397,7 @@ fn legendre_symbol(a: &BigUint, p: &BigUint) -> i8 { // Ported from sympy implementation // Simplified as a & p are nonnegative // Asumes p is a prime number -pub(crate) fn is_quad_residue(a: &BigUint, p: &BigUint) -> Result { +pub fn is_quad_residue(a: &BigUint, p: &BigUint) -> Result { if p.is_zero() { return Err(MathError::IsQuadResidueZeroPrime); } diff --git a/vm/src/tests/mod.rs b/vm/src/tests/mod.rs index 2746df2252..331c1d3e06 100644 --- a/vm/src/tests/mod.rs +++ b/vm/src/tests/mod.rs @@ -10,7 +10,10 @@ use crate::Felt252; use crate::{ hint_processor::cairo_1_hint_processor::hint_processor::Cairo1HintProcessor, types::{builtin_name::BuiltinName, relocatable::MaybeRelocatable}, - vm::runners::cairo_runner::{CairoArg, CairoRunner}, + vm::runners::{ + cairo_function_runner::{CairoFunctionRunner, EntryPoint}, + cairo_runner::CairoArg, + }, }; #[cfg(feature = "cairo-1-hints")] use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass; @@ -104,7 +107,7 @@ fn run_cairo_1_entrypoint( let mut hint_processor = Cairo1HintProcessor::new(&contract_class.hints, RunResources::default(), false); - let mut runner = CairoRunner::new( + let mut function_runner = CairoFunctionRunner::new_custom( &(contract_class.clone().try_into().unwrap()), LayoutName::all_cairo, None, @@ -115,16 +118,16 @@ fn run_cairo_1_entrypoint( .unwrap(); let program_builtins = get_casm_contract_builtins(&contract_class, entrypoint_offset); - runner + function_runner .initialize_function_runner_cairo_1(&program_builtins) .unwrap(); // Implicit Args - let syscall_segment = MaybeRelocatable::from(runner.vm.add_memory_segment()); + let syscall_segment = MaybeRelocatable::from(function_runner.vm.add_memory_segment()); - let builtins = runner.get_program_builtins(); + let builtins = function_runner.get_program_builtins(); - let builtin_segment: Vec = runner + let builtin_segment: Vec = function_runner .vm .get_builtin_runners() .iter() @@ -141,27 +144,33 @@ fn run_cairo_1_entrypoint( // Other args // Load builtin costs - let builtin_costs: Vec = - vec![0.into(), 0.into(), 0.into(), 0.into(), 0.into()]; - let builtin_costs_ptr = runner.vm.add_memory_segment(); - runner + let builtin_costs: Vec = vec![ + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + ]; + let builtin_costs_ptr = function_runner.vm.add_memory_segment(); + function_runner .vm .load_data(builtin_costs_ptr, &builtin_costs) .unwrap(); // Load extra data - let core_program_end_ptr = - (runner.program_base.unwrap() + runner.program.shared_program_data.data.len()).unwrap(); + let core_program_end_ptr = (function_runner.program_base.unwrap() + + function_runner.program.shared_program_data.data.len()) + .unwrap(); let program_extra_data: Vec = - vec![0x208B7FFF7FFF7FFE.into(), builtin_costs_ptr.into()]; - runner + vec![0x208B7FFF7FFF7FFE_u64.into(), builtin_costs_ptr.into()]; + function_runner .vm .load_data(core_program_end_ptr, &program_extra_data) .unwrap(); // Load calldata - let calldata_start = runner.vm.add_memory_segment(); - let calldata_end = runner.vm.load_data(calldata_start, args).unwrap(); + let calldata_start = function_runner.vm.add_memory_segment(); + let calldata_end = function_runner.vm.load_data(calldata_start, args).unwrap(); // Create entrypoint_args @@ -173,25 +182,26 @@ fn run_cairo_1_entrypoint( MaybeRelocatable::from(calldata_start).into(), MaybeRelocatable::from(calldata_end).into(), ]); - let entrypoint_args: Vec<&CairoArg> = entrypoint_args.iter().collect(); // Run contract entrypoint - runner - .run_from_entrypoint( - entrypoint_offset, - &entrypoint_args, + let program_segment_size = + function_runner.program.shared_program_data.data.len() + program_extra_data.len(); + function_runner + .run( + EntryPoint::Pc(entrypoint_offset), true, - Some(runner.program.shared_program_data.data.len() + program_extra_data.len()), + Some(program_segment_size), &mut hint_processor, + &entrypoint_args, ) .unwrap(); // Check return values - let return_values = runner.vm.get_return_values(5).unwrap(); + let return_values = function_runner.vm.get_return_values(5).unwrap(); let retdata_start = return_values[3].get_relocatable().unwrap(); let retdata_end = return_values[4].get_relocatable().unwrap(); - let retdata: Vec = runner + let retdata: Vec = function_runner .vm .get_integer_range(retdata_start, (retdata_end - retdata_start).unwrap()) .unwrap() @@ -211,7 +221,7 @@ fn run_cairo_1_entrypoint_with_run_resources( hint_processor: &mut Cairo1HintProcessor, args: &[MaybeRelocatable], ) -> Result, CairoRunError> { - let mut runner = CairoRunner::new( + let mut function_runner = CairoFunctionRunner::new_custom( &(contract_class.clone().try_into().unwrap()), LayoutName::all_cairo, None, @@ -222,16 +232,16 @@ fn run_cairo_1_entrypoint_with_run_resources( .unwrap(); let program_builtins = get_casm_contract_builtins(&contract_class, entrypoint_offset); - runner + function_runner .initialize_function_runner_cairo_1(&program_builtins) .unwrap(); // Implicit Args - let syscall_segment = MaybeRelocatable::from(runner.vm.add_memory_segment()); + let syscall_segment = MaybeRelocatable::from(function_runner.vm.add_memory_segment()); - let builtins = runner.get_program_builtins(); + let builtins = function_runner.get_program_builtins(); - let builtin_segment: Vec = runner + let builtin_segment: Vec = function_runner .vm .get_builtin_runners() .iter() @@ -248,27 +258,33 @@ fn run_cairo_1_entrypoint_with_run_resources( // Other args // Load builtin costs - let builtin_costs: Vec = - vec![0.into(), 0.into(), 0.into(), 0.into(), 0.into()]; - let builtin_costs_ptr = runner.vm.add_memory_segment(); - runner + let builtin_costs: Vec = vec![ + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + ]; + let builtin_costs_ptr = function_runner.vm.add_memory_segment(); + function_runner .vm .load_data(builtin_costs_ptr, &builtin_costs) .unwrap(); // Load extra data - let core_program_end_ptr = - (runner.program_base.unwrap() + runner.program.shared_program_data.data.len()).unwrap(); + let core_program_end_ptr = (function_runner.program_base.unwrap() + + function_runner.program.shared_program_data.data.len()) + .unwrap(); let program_extra_data: Vec = - vec![0x208B7FFF7FFF7FFE.into(), builtin_costs_ptr.into()]; - runner + vec![0x208B7FFF7FFF7FFE_u64.into(), builtin_costs_ptr.into()]; + function_runner .vm .load_data(core_program_end_ptr, &program_extra_data) .unwrap(); // Load calldata - let calldata_start = runner.vm.add_memory_segment(); - let calldata_end = runner.vm.load_data(calldata_start, args).unwrap(); + let calldata_start = function_runner.vm.add_memory_segment(); + let calldata_end = function_runner.vm.load_data(calldata_start, args).unwrap(); // Create entrypoint_args @@ -280,23 +296,24 @@ fn run_cairo_1_entrypoint_with_run_resources( MaybeRelocatable::from(calldata_start).into(), MaybeRelocatable::from(calldata_end).into(), ]); - let entrypoint_args: Vec<&CairoArg> = entrypoint_args.iter().collect(); // Run contract entrypoint - runner.run_from_entrypoint( - entrypoint_offset, - &entrypoint_args, + let program_segment_size = + function_runner.program.shared_program_data.data.len() + program_extra_data.len(); + function_runner.run( + EntryPoint::Pc(entrypoint_offset), true, - Some(runner.program.shared_program_data.data.len() + program_extra_data.len()), + Some(program_segment_size), hint_processor, + &entrypoint_args, )?; // Check return values - let return_values = runner.vm.get_return_values(5).unwrap(); + let return_values = function_runner.vm.get_return_values(5).unwrap(); let retdata_start = return_values[3].get_relocatable().unwrap(); let retdata_end = return_values[4].get_relocatable().unwrap(); - let retdata: Vec = runner + let retdata: Vec = function_runner .vm .get_integer_range(retdata_start, (retdata_end - retdata_start).unwrap()) .unwrap() diff --git a/vm/src/types/errors/program_errors.rs b/vm/src/types/errors/program_errors.rs index d972a17068..43bc5d3858 100644 --- a/vm/src/types/errors/program_errors.rs +++ b/vm/src/types/errors/program_errors.rs @@ -19,6 +19,10 @@ pub enum ProgramError { StrippedProgramNoMain, #[error("Hint PC ({0}) is greater or equal to program length ({1})")] InvalidHintPc(usize, usize), + #[error("Identifier \"{0}\" is type alias but has no destination")] + AliasMissingDestination(String), + #[error("invalid identifier type \"{1}\" for \"{0}\": expected \"alias\" or \"function\"")] + InvalidIdentifierTypeForPc(String, String), } #[cfg(test)] @@ -31,4 +35,27 @@ mod tests { let formatted_error = format!("{error}"); assert_eq!(formatted_error, "Entrypoint my_function not found"); } + + #[test] + fn format_alias_missing_destination_error() { + let error = ProgramError::AliasMissingDestination(String::from("__main__.assert_nn")); + let formatted_error = format!("{error}"); + assert_eq!( + formatted_error, + "Identifier \"__main__.assert_nn\" is type alias but has no destination" + ); + } + + #[test] + fn format_invalid_identifier_type_for_pc_error() { + let error = ProgramError::InvalidIdentifierTypeForPc( + String::from("__main__.my_struct"), + String::from("struct"), + ); + let formatted_error = format!("{error}"); + assert_eq!( + formatted_error, + "invalid identifier type \"struct\" for \"__main__.my_struct\": expected \"alias\" or \"function\"" + ); + } } diff --git a/vm/src/types/relocatable.rs b/vm/src/types/relocatable.rs index 37d1358624..f360b4e419 100644 --- a/vm/src/types/relocatable.rs +++ b/vm/src/types/relocatable.rs @@ -7,6 +7,7 @@ use crate::Felt252; use crate::{ relocatable, types::errors::math_errors::MathError, vm::errors::memory_errors::MemoryError, }; +use num_bigint::{BigInt, BigUint}; use num_traits::ToPrimitive; use serde::{Deserialize, Serialize}; @@ -58,12 +59,6 @@ impl From<(isize, usize)> for MaybeRelocatable { } } -impl From for MaybeRelocatable { - fn from(num: usize) -> Self { - MaybeRelocatable::Int(Felt252::from(num)) - } -} - impl From for MaybeRelocatable { fn from(num: Felt252) -> Self { MaybeRelocatable::Int(num) @@ -94,6 +89,25 @@ impl From for MaybeRelocatable { } } +// Implement primitive and big-int (owned + reference) conversions by first converting to Felt252, +// then wrapping as MaybeRelocatable::Int. +macro_rules! impl_from_for_maybe_relocatable { + ($($t:ty),* $(,)?) => { + $( + impl From<$t> for MaybeRelocatable { + fn from(num: $t) -> Self { + MaybeRelocatable::Int(Felt252::from(num)) + } + } + )* + }; +} + +impl_from_for_maybe_relocatable!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, BigUint, BigInt, &BigUint, + &BigInt +); + impl Display for MaybeRelocatable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -396,6 +410,7 @@ mod tests { use super::*; use crate::{felt_hex, felt_str}; use crate::{relocatable, utils::test_utils::mayberelocatable}; + use num_bigint::{BigInt, BigUint}; use proptest::prelude::*; @@ -429,6 +444,45 @@ mod tests { } } + #[test] + // Verifies primitive integers convert into `MaybeRelocatable::Int` via the macro-generated impls. + fn maybe_relocatable_from_primitive_via_macro() { + let value = MaybeRelocatable::from(42_u8); + assert_eq!(value, MaybeRelocatable::Int(Felt252::from(42_u8))); + } + + #[test] + // Verifies owned `BigUint`/`BigInt` values convert into `MaybeRelocatable::Int`. + fn maybe_relocatable_from_owned_bigints_via_macro() { + let big_uint = BigUint::from(123_u32); + let big_int = BigInt::from(456_i32); + + assert_eq!( + MaybeRelocatable::from(big_uint), + MaybeRelocatable::Int(Felt252::from(123_u32)) + ); + assert_eq!( + MaybeRelocatable::from(big_int), + MaybeRelocatable::Int(Felt252::from(456_i32)) + ); + } + + #[test] + // Verifies referenced `&BigUint`/`&BigInt` values convert into `MaybeRelocatable::Int`. + fn maybe_relocatable_from_referenced_bigints_via_macro() { + let big_uint = BigUint::from(789_u32); + let big_int = BigInt::from(321_i32); + + assert_eq!( + MaybeRelocatable::from(&big_uint), + MaybeRelocatable::Int(Felt252::from(789_u32)) + ); + assert_eq!( + MaybeRelocatable::from(&big_int), + MaybeRelocatable::Int(Felt252::from(321_i32)) + ); + } + #[test] fn add_bigint_to_int() { let addr = MaybeRelocatable::from(Felt252::from(7i32)); diff --git a/vm/src/vm/runners/cairo_function_runner.rs b/vm/src/vm/runners/cairo_function_runner.rs new file mode 100644 index 0000000000..aec0b6df4b --- /dev/null +++ b/vm/src/vm/runners/cairo_function_runner.rs @@ -0,0 +1,333 @@ +//! A Cairo function runner for testing purposes. +//! +//! This module provides [`CairoFunctionRunner`], a high-level interface for running individual +//! Cairo 0 functions with automatic builtin initialization. It allows direct invocation of specific +//! entrypoints with custom arguments. + +use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor; +use crate::hint_processor::hint_processor_definition::HintProcessor; +use crate::serde::deserialize_program::Identifier; +use crate::types::builtin_name::BuiltinName; +use crate::types::errors::program_errors::ProgramError; +use crate::types::instance_definitions::mod_instance_def::ModInstanceDef; +use crate::types::layout::CairoLayoutParams; +use crate::types::layout_name::LayoutName; +use crate::types::program::Program; +use crate::types::relocatable::MaybeRelocatable; +use crate::vm::errors::cairo_run_errors::CairoRunError; +use crate::vm::errors::memory_errors::MemoryError; +use crate::vm::errors::runner_errors::RunnerError; +use crate::vm::errors::vm_errors::VirtualMachineError; +use crate::vm::errors::vm_exception::VmException; +use crate::vm::runners::builtin_runner::{ + BitwiseBuiltinRunner, EcOpBuiltinRunner, HashBuiltinRunner, KeccakBuiltinRunner, + ModBuiltinRunner, OutputBuiltinRunner, PoseidonBuiltinRunner, RangeCheckBuiltinRunner, + SignatureBuiltinRunner, RC_N_PARTS_96, RC_N_PARTS_STANDARD, +}; +use crate::vm::runners::cairo_runner::{CairoArg, CairoRunner}; +use crate::vm::security::verify_secure_runner; + +/// Identifies a Cairo function entrypoint either by function name or by program counter. +pub enum EntryPoint<'a> { + Name(&'a str), + Pc(usize), +} + +/// A runner for executing individual Cairo functions. +/// Used for testing purposes only. +pub struct CairoFunctionRunner { + /// The Cairo runner instance that manages VM execution. + pub runner: CairoRunner, +} + +/// Pushes a builtin runner into `runner.vm.builtin_runners` after converting it with `.into()`. +/// +/// Example expansion: +/// `push_builtin!(runner, HashBuiltinRunner::new(Some(32), true));` +/// becomes: +/// `runner.vm.builtin_runners.push(HashBuiltinRunner::new(Some(32), true).into());` +macro_rules! push_builtin { + ($runner:expr, $builtin:expr) => { + $runner.vm.builtin_runners.push($builtin.into()); + }; +} + +impl std::ops::Deref for CairoFunctionRunner { + type Target = CairoRunner; + + fn deref(&self) -> &Self::Target { + &self.runner + } +} + +impl std::ops::DerefMut for CairoFunctionRunner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.runner + } +} + +impl CairoFunctionRunner { + /// Creates a new `CairoFunctionRunner`. + /// + /// Initializes a basic `CairoRunner` with: + /// - `LayoutName::plain` + /// - `dynamic_layout_params = None` + /// - `proof_mode = false` + /// - `trace_enabled = false` + /// - `disable_trace_padding = false` + /// + /// and then preloads a fixed set of commonly used builtins. + /// + /// # Arguments + /// - `program`: The compiled Cairo program to execute. + /// + /// # Returns + /// - `Ok(CairoFunctionRunner)`: On successful initialization. + /// - `Err(CairoRunError)`: If the runner cannot be created. + #[allow(clippy::result_large_err)] + pub fn new(program: &Program) -> std::result::Result { + let mut runner = CairoRunner::new( + program, + LayoutName::plain, + None, // dynamic_layout_params + false, // proof_mode + false, // trace_enabled + false, // disable_trace_padding + )?; + + Self::initialize_all_builtins(&mut runner)?; + runner.initialize_segments(None); + + Ok(Self { runner }) + } + /// Creates a new `CairoFunctionRunner` with custom `CairoRunner` initialization parameters. + /// + /// Unlike [`Self::new`], this constructor does not preload builtins or initialize segments. + #[allow(clippy::result_large_err)] + pub fn new_custom( + program: &Program, + layout: LayoutName, + dynamic_layout_params: Option, + proof_mode: bool, + trace_enabled: bool, + disable_trace_padding: bool, + ) -> std::result::Result { + let runner = CairoRunner::new( + program, + layout, + dynamic_layout_params, + proof_mode, + trace_enabled, + disable_trace_padding, + )?; + + Ok(Self { runner }) + } + /// Initializes a fixed set of 11 builtins used by this function runner. + fn initialize_all_builtins(runner: &mut CairoRunner) -> Result<(), RunnerError> { + runner.vm.builtin_runners.clear(); + + push_builtin!(runner, HashBuiltinRunner::new(Some(32), true)); + push_builtin!( + runner, + RangeCheckBuiltinRunner::::new(Some(1), true) + ); + push_builtin!(runner, OutputBuiltinRunner::new(true)); + push_builtin!(runner, SignatureBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, BitwiseBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, EcOpBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, KeccakBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, PoseidonBuiltinRunner::new(Some(1), true)); + push_builtin!( + runner, + RangeCheckBuiltinRunner::::new(Some(1), true) + ); + push_builtin!( + runner, + ModBuiltinRunner::new_add_mod(&ModInstanceDef::new(Some(1), 1, 96), true) + ); + push_builtin!( + runner, + ModBuiltinRunner::new_mul_mod(&ModInstanceDef::new(Some(1), 1, 96), true) + ); + + Ok(()) + } + /// Runs a Cairo function from the specified entrypoint. + /// + /// # Arguments + /// - `entrypoint`: The entrypoint to execute, either by function name or by PC. + /// - `verify_secure`: If `true`, runs additional security verification after execution. + /// - `program_segment_size`: Optional size limit for the program segment. + /// - `program_input`: Optional program input to inject into the execution scopes. + /// - `hint_processor`: The hint processor used during VM execution. + /// - `args`: The function arguments. + /// + /// # Returns + /// - `Ok(())`: On successful execution. + /// - `Err(CairoRunError)`: If the entrypoint is not found, execution fails, or security + /// verification fails. + #[allow(clippy::result_large_err)] + pub fn run( + &mut self, + entrypoint: EntryPoint<'_>, + verify_secure: bool, + program_segment_size: Option, + hint_processor: &mut dyn HintProcessor, + args: &[CairoArg], + ) -> std::result::Result<(), CairoRunError> { + let entrypoint_pc = match entrypoint { + EntryPoint::Name(name) => self.get_function_pc(name)?, + EntryPoint::Pc(pc) => pc, + }; + + let cairo_args: Vec<&CairoArg> = args.iter().collect(); + + self.run_from_entrypoint( + entrypoint_pc, + &cairo_args, + verify_secure, + program_segment_size, + hint_processor, + )?; + + Ok(()) + } + + #[allow(clippy::result_large_err)] + // Builds the call stack from Cairo args, runs until the function's end PC, and optionally verifies security constraints. + fn run_from_entrypoint( + &mut self, + entrypoint: usize, + args: &[&CairoArg], + verify_secure: bool, + program_segment_size: Option, + hint_processor: &mut dyn HintProcessor, + ) -> std::result::Result<(), CairoRunError> { + let stack = args + .iter() + .map(|arg| self.vm.segments.gen_cairo_arg(arg)) + .collect::, VirtualMachineError>>()?; + let return_fp = MaybeRelocatable::from(0_i64); + let end = self.initialize_function_entrypoint(entrypoint, stack, return_fp)?; + + self.initialize_vm()?; + + self.run_until_pc(end, hint_processor) + .map_err(|err| VmException::from_vm_error(self, err))?; + let is_proof_mode = self.is_proof_mode(); + self.end_run(true, false, hint_processor, is_proof_mode)?; + + if verify_secure { + verify_secure_runner(self, false, program_segment_size)?; + } + + Ok(()) + } + + /// Runs a Cairo 0 function with a default empty `BuiltinHintProcessor`for cairo0 files. + #[allow(clippy::result_large_err)] + pub fn run_default_cairo0( + &mut self, + entrypoint: &str, + args: &[CairoArg], + ) -> std::result::Result<(), CairoRunError> { + let mut hint_processor = BuiltinHintProcessor::new_empty(); + self.run( + EntryPoint::Name(entrypoint), + false, + None, + &mut hint_processor, + args, + ) + } + + /// Retrieves return values from the VM's memory after function execution. + /// + /// Reads the last `n_return_values` values from the allocation pointer (AP). + pub fn get_return_values( + &self, + n_return_values: usize, + ) -> Result, MemoryError> { + self.vm.get_return_values(n_return_values) + } + + /// Gets the base pointer for a specific builtin. + pub fn get_builtin_base(&self, builtin_name: BuiltinName) -> Option { + self.vm + .builtin_runners + .iter() + .find(|builtin_runner| builtin_runner.name() == builtin_name) + .map(|builtin_runner| MaybeRelocatable::from((builtin_runner.base() as isize, 0))) + } + + /// Returns the program counter (PC) for a function entrypoint by name. + /// + /// Looks up the identifier `__main__.{entrypoint}` in the program, then resolves it to a PC + /// (following alias chains if needed) via [`get_pc_from_identifier`](Self::get_pc_from_identifier). + /// + /// # Errors + /// - [`ProgramError::EntrypointNotFound`] if no identifier exists for the given name. + /// - [`RunnerError::NoPC`] if the resolved identifier has no PC (e.g. corrupt alias). + /// - [`ProgramError::AliasMissingDestination`] if an alias has no `destination`. + /// - [`ProgramError::InvalidIdentifierTypeForPc`] if the identifier type is not `"function"` or `"alias"`. + #[allow(clippy::result_large_err)] + pub(crate) fn get_function_pc( + &self, + entrypoint: &str, + ) -> std::result::Result { + let full_name = format!("__main__.{entrypoint}"); + let identifier = self + .program + .get_identifier(&full_name) + .ok_or_else(|| ProgramError::EntrypointNotFound(entrypoint.to_string()))?; + + self.get_pc_from_identifier(identifier) + } + + /// Resolves an identifier to its program counter (PC), following alias chains. + /// + /// - **`function`**: returns the identifier's `pc` if present. + /// - **`alias`**: resolves `destination` to another identifier and recurses until a function is found. + /// - **Other types** (e.g. `struct`, `const`): returns [`ProgramError::InvalidIdentifierTypeForPc`]. + /// + /// # Errors + /// - [`RunnerError::NoPC`] when the identifier is a function but has no `pc`. + /// - [`ProgramError::AliasMissingDestination`] when the identifier is an alias but has no `destination`. + /// - [`ProgramError::EntrypointNotFound`] when the alias destination is not in the program. + /// - [`ProgramError::InvalidIdentifierTypeForPc`] when the identifier type is not `"function"` or `"alias"`. + #[allow(clippy::result_large_err)] + fn get_pc_from_identifier( + &self, + idetifier: &Identifier, + ) -> std::result::Result { + match idetifier.type_.as_deref() { + Some("function") => { + let pc = idetifier.pc.ok_or(RunnerError::NoPC)?; + Ok(pc) + } + Some("alias") => { + let destination = idetifier.destination.as_deref().ok_or( + ProgramError::AliasMissingDestination( + idetifier.full_name.as_deref().unwrap_or("").to_string(), + ), + )?; + + let destination_identifier = self + .runner + .program + .get_identifier(destination) + .ok_or_else(|| ProgramError::EntrypointNotFound(destination.to_string()))?; + self.get_pc_from_identifier(destination_identifier) + } + v => { + let name = idetifier + .full_name + .clone() + .unwrap_or_else(|| "".to_string()); + let type_str = v.unwrap_or("").to_string(); + Err(ProgramError::InvalidIdentifierTypeForPc(name, type_str).into()) + } + } + } +} diff --git a/vm/src/vm/runners/cairo_function_runner_test.rs b/vm/src/vm/runners/cairo_function_runner_test.rs new file mode 100644 index 0000000000..ea9ba6fb5f --- /dev/null +++ b/vm/src/vm/runners/cairo_function_runner_test.rs @@ -0,0 +1,289 @@ +// CairoFunctionRunner unit tests. +// Tested functions: new, new_custom, run, run_default_cairo0, get_builtin_base, get_return_values. +use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor; +use crate::types::builtin_name::BuiltinName; +use crate::types::errors::program_errors::ProgramError; +use crate::types::layout_name::LayoutName; +use crate::types::program::Program; +use crate::types::relocatable::MaybeRelocatable; +use crate::vm::errors::cairo_run_errors::CairoRunError; +use crate::vm::runners::cairo_function_runner::{CairoFunctionRunner, EntryPoint}; +use crate::vm::runners::cairo_runner::CairoArg; +use assert_matches::assert_matches; + +fn load_program(program_bytes: &[u8]) -> Program { + Program::from_bytes(program_bytes, None).unwrap() +} + +#[test] +// Test that `new` initializes all 11 builtins, creates program/execution segments, and does not initialize segment_arena. +fn new_initializes_expected_builtin_bases() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + assert_eq!(function_runner.runner.vm.builtin_runners.len(), 11); + let expected_builtins = [ + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::output, + BuiltinName::ecdsa, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::keccak, + BuiltinName::poseidon, + BuiltinName::range_check96, + BuiltinName::add_mod, + BuiltinName::mul_mod, + ]; + + for builtin in expected_builtins { + assert!(function_runner.get_builtin_base(builtin).is_some()); + } + assert!(function_runner + .get_builtin_base(BuiltinName::segment_arena) + .is_none()); + assert_eq!(function_runner.runner.vm.segments.num_segments(), 11 + 2); +} + +#[test] +// Test that `new_custom` does not initialize builtins or memory segments automatically. +fn new_custom_does_not_initialize_builtins_or_segments() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = + CairoFunctionRunner::new_custom(&program, LayoutName::plain, None, false, false, false) + .unwrap(); + + assert!(function_runner + .get_builtin_base(BuiltinName::range_check) + .is_none()); + assert_eq!(function_runner.runner.vm.segments.num_segments(), 0); +} + +#[test] +// Test successful function execution by entrypoint name for multiple functions in one program. +fn run_from_entrypoint_custom_program_test() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let main_args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr.clone()), + ]; + assert_matches!( + function_runner.run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &main_args, + ), + Ok(()) + ); + + let mut second_function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut second_hint_processor = BuiltinHintProcessor::new_empty(); + let second_range_check_ptr = second_function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let fib_args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(second_range_check_ptr), + ]; + assert_matches!( + second_function_runner.run( + EntryPoint::Name("evaluate_fib"), + true, + None, + &mut second_hint_processor, + &fib_args, + ), + Ok(()) + ); +} + +#[test] +// Test successful execution using `EntryPoint::Pc` instead of function name lookup. +fn run_by_program_counter_happy_path() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr), + ]; + let entrypoint_pc = function_runner + .runner + .program + .get_identifier("__main__.main") + .unwrap() + .pc + .unwrap(); + + assert_matches!( + function_runner.run( + EntryPoint::Pc(entrypoint_pc), + true, + None, + &mut hint_processor, + &args, + ), + Ok(()) + ); +} + +#[test] +// Test `run_default_cairo0` happy path and verify zero requested return values. +fn run_default_cairo0_happy_path() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr), + ]; + + assert_matches!(function_runner.run_default_cairo0("main", &args), Ok(())); + assert_eq!(function_runner.get_return_values(0).unwrap(), vec![]); +} + +#[test] +// Test that get_function_pc resolves "__main__.assert_nn" (alias) to the PC of starkware.cairo.common.math.assert_nn (0). +fn get_function_pc_assert_nn_resolves_alias_to_pc_0() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + let pc = function_runner.get_function_pc("assert_nn").unwrap(); + assert_eq!( + pc, 0, + "assert_nn is an alias to starkware.cairo.common.math.assert_nn which has pc 0" + ); +} + +#[test] +// Test that get_function_pc returns the direct PC for "__main__.assert_nn_manual_implementation" (function with pc 4). +fn get_function_pc_assert_nn_manual_implementation_returns_pc_4() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + let pc = function_runner + .get_function_pc("assert_nn_manual_implementation") + .unwrap(); + assert_eq!( + pc, 4, + "assert_nn_manual_implementation is a function with pc 4" + ); +} + +#[test] +// Test that running a missing function name returns `EntrypointNotFound`. +fn run_missing_entrypoint_returns_entrypoint_not_found() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + + assert_matches!( + function_runner.run( + EntryPoint::Name("missing_entrypoint"), + false, + None, + &mut hint_processor, + &[], + ), + Err(CairoRunError::Program(ProgramError::EntrypointNotFound(entrypoint))) + if entrypoint == "missing_entrypoint" + ); +} + +#[test] +// Test that `run_default_cairo0` propagates missing entrypoint errors. +fn run_default_cairo0_missing_entrypoint_returns_entrypoint_not_found() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + + assert_matches!( + function_runner.run_default_cairo0("missing_entrypoint", &[]), + Err(CairoRunError::Program(ProgramError::EntrypointNotFound(entrypoint))) + if entrypoint == "missing_entrypoint" + ); +} + +#[test] +// Test bitwise builtin execution and verify no memory holes remain. +fn run_from_entrypoint_bitwise_test_check_memory_holes() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/bitwise_builtin_test.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let bitwise_ptr = function_runner + .get_builtin_base(BuiltinName::bitwise) + .unwrap(); + let args = vec![CairoArg::from(bitwise_ptr)]; + + assert!(function_runner + .run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &args, + ) + .is_ok()); + + assert_eq!(function_runner.runner.get_memory_holes().unwrap(), 0); +} + +#[test] +// Test VM exception error message substitution from `error_msg` attributes. +fn run_from_entrypoint_substitute_error_message_test() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/bad_programs/error_msg_function.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let result = function_runner.run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &[], + ); + + match result { + Err(CairoRunError::VmException(exception)) => { + assert_eq!( + exception.error_attr_value, + Some(String::from("Error message: Test error\n")) + ) + } + Err(_) => panic!("Wrong error returned, expected VmException"), + Ok(_) => panic!("Expected run to fail"), + } +} diff --git a/vm/src/vm/runners/cairo_runner.rs b/vm/src/vm/runners/cairo_runner.rs index 82f620292b..0dd41ab8f5 100644 --- a/vm/src/vm/runners/cairo_runner.rs +++ b/vm/src/vm/runners/cairo_runner.rs @@ -30,14 +30,11 @@ use crate::{ utils::is_subsequence, vm::{ errors::{ - cairo_run_errors::CairoRunError, memory_errors::{InsufficientAllocatedCellsError, MemoryError}, runner_errors::RunnerError, trace_errors::TraceError, vm_errors::VirtualMachineError, - vm_exception::VmException, }, - security::verify_secure_runner, { runners::builtin_runner::{ BitwiseBuiltinRunner, BuiltinRunner, EcOpBuiltinRunner, HashBuiltinRunner, @@ -83,18 +80,36 @@ pub enum CairoArg { Composed(Vec), } -impl From for CairoArg { - fn from(other: MaybeRelocatable) -> Self { - CairoArg::Single(other) +// Converts a vector of values into an array-style Cairo argument. +impl From> for CairoArg +where + T: Into, +{ + fn from(other: Vec) -> Self { + CairoArg::Array(other.into_iter().map(Into::into).collect()) } } -impl From> for CairoArg { - fn from(other: Vec) -> Self { - CairoArg::Array(other) +// Converts a single value into a single-item Cairo argument. +impl From for CairoArg +where + T: Into, +{ + fn from(other: T) -> Self { + CairoArg::Single(other.into()) } } +/// Creates a `Vec` from a list of expressions. +/// +/// Each expression is converted using `From for CairoArg`. +#[macro_export] +macro_rules! cairo_args { + ($($x:expr),* $(,)?) => { + vec![$($crate::vm::runners::cairo_runner::CairoArg::from($x)),*] + }; +} + // ================ // RunResources // ================ @@ -559,7 +574,7 @@ impl CairoRunner { Ok(()) } - fn is_proof_mode(&self) -> bool { + pub fn is_proof_mode(&self) -> bool { self.runner_mode == RunnerMode::ProofModeCanonical || self.runner_mode == RunnerMode::ProofModeCairo1 } @@ -1309,38 +1324,6 @@ impl CairoRunner { Ok(()) } - #[allow(clippy::result_large_err)] - /// Runs a cairo program from a give entrypoint, indicated by its pc offset, with the given arguments. - /// If `verify_secure` is set to true, [verify_secure_runner] will be called to run extra verifications. - /// `program_segment_size` is only used by the [verify_secure_runner] function and will be ignored if `verify_secure` is set to false. - pub fn run_from_entrypoint( - &mut self, - entrypoint: usize, - args: &[&CairoArg], - verify_secure: bool, - program_segment_size: Option, - hint_processor: &mut dyn HintProcessor, - ) -> Result<(), CairoRunError> { - let stack = args - .iter() - .map(|arg| self.vm.segments.gen_cairo_arg(arg)) - .collect::, VirtualMachineError>>()?; - let return_fp = MaybeRelocatable::from(0); - let end = self.initialize_function_entrypoint(entrypoint, stack, return_fp)?; - - self.initialize_vm()?; - - self.run_until_pc(end, hint_processor) - .map_err(|err| VmException::from_vm_error(self, err))?; - self.end_run(true, false, hint_processor, self.is_proof_mode())?; - - if verify_secure { - verify_secure_runner(self, false, program_segment_size)?; - } - - Ok(()) - } - // Returns Ok(()) if there are enough allocated cells for the builtins. // If not, the number of steps should be increased or a different layout should be used. pub fn check_used_cells(&self) -> Result<(), VirtualMachineError> { @@ -4956,107 +4939,6 @@ mod tests { assert_eq!(bitwise_builtin.stop_ptr, Some(5)); } - #[test] - fn run_from_entrypoint_custom_program_test() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/example_program.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_builtins(false).unwrap(); - cairo_runner.initialize_segments(None); - assert_matches!( - cairo_runner.run_from_entrypoint( - main_entrypoint, - &[ - &mayberelocatable!(2).into(), - &MaybeRelocatable::from((2, 0)).into() - ], //range_check_ptr - true, - None, - &mut hint_processor, - ), - Ok(()) - ); - - let mut new_cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - new_cairo_runner.initialize_builtins(false).unwrap(); - new_cairo_runner.initialize_segments(None); - - let fib_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.evaluate_fib") - .unwrap() - .pc - .unwrap(); - - assert_matches!( - new_cairo_runner.run_from_entrypoint( - fib_entrypoint, - &[ - &mayberelocatable!(2).into(), - &MaybeRelocatable::from((2, 0)).into() - ], - true, - None, - &mut hint_processor, - ), - Ok(()) - ); - } - - #[test] - fn run_from_entrypoint_bitwise_test_check_memory_holes() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/bitwise_builtin_test.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_function_runner().unwrap(); - - assert!(cairo_runner - .run_from_entrypoint( - main_entrypoint, - &[ - &MaybeRelocatable::from((2, 0)).into() //bitwise_ptr - ], - true, - None, - &mut hint_processor, - ) - .is_ok()); - - // Check that memory_holes == 0 - assert!(cairo_runner.get_memory_holes().unwrap().is_zero()); - } - #[test] fn cairo_arg_from_single() { let expected = CairoArg::Single(MaybeRelocatable::from((0, 0))); @@ -5071,6 +4953,34 @@ mod tests { assert_eq!(expected, value.into()) } + #[test] + // Verifies converting `Vec` into `CairoArg` produces the expected array argument. + fn cairo_arg_from_vec_of_ints() { + let expected = CairoArg::Array(vec![ + MaybeRelocatable::from(Felt252::from(1)), + MaybeRelocatable::from(Felt252::from(2)), + MaybeRelocatable::from(Felt252::from(3)), + ]); + let value = vec![1_i32, 2_i32, 3_i32]; + assert_eq!(expected, CairoArg::from(value)) + } + + #[test] + // Verifies `cairo_args!` builds a `Vec` from mixed argument types. + fn cairo_args_macro_builds_vec_of_cairo_args() { + let expected = vec![ + CairoArg::Single(MaybeRelocatable::from(Felt252::from(7))), + CairoArg::Single(MaybeRelocatable::from((1, 3))), + CairoArg::Array(vec![ + MaybeRelocatable::from(Felt252::from(9)), + MaybeRelocatable::from(Felt252::from(10)), + ]), + ]; + + let args = crate::cairo_args![7_i32, (1, 3), vec![9_i32, 10_i32],]; + assert_eq!(args, expected); + } + fn setup_execution_resources() -> (ExecutionResources, ExecutionResources) { let mut builtin_instance_counter: BTreeMap = BTreeMap::new(); builtin_instance_counter.insert(BuiltinName::output, 8); @@ -5132,42 +5042,6 @@ mod tests { .contains_key(&BuiltinName::range_check)); } - #[test] - fn run_from_entrypoint_substitute_error_message_test() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/bad_programs/error_msg_function.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_builtins(false).unwrap(); - cairo_runner.initialize_segments(None); - - let result = - cairo_runner.run_from_entrypoint(main_entrypoint, &[], true, None, &mut hint_processor); - match result { - Err(CairoRunError::VmException(exception)) => { - assert_eq!( - exception.error_attr_value, - Some(String::from("Error message: Test error\n")) - ) - } - Err(_) => panic!("Wrong error returned, expected VmException"), - Ok(_) => panic!("Expected run to fail"), - } - } - #[test] fn get_builtins_final_stack_range_check_builtin() { let program = Program::from_bytes( diff --git a/vm/src/vm/runners/mod.rs b/vm/src/vm/runners/mod.rs index ed5ecaa7dc..96f93f8989 100644 --- a/vm/src/vm/runners/mod.rs +++ b/vm/src/vm/runners/mod.rs @@ -1,3 +1,6 @@ pub mod builtin_runner; +pub mod cairo_function_runner; +#[cfg(test)] +mod cairo_function_runner_test; pub mod cairo_pie; pub mod cairo_runner;