From a1e3635f4d91657e5987c66c59f7472902a9cfa7 Mon Sep 17 00:00:00 2001 From: Avi Cohen Date: Sun, 31 May 2026 16:32:52 +0300 Subject: [PATCH 1/3] add ContractExecutor dispatch enum (Aot + Emu) Adds a public dispatch enum so a single call site can pick between the AOT executor and the sierra-emu interpreter at runtime, without forcing every caller to maintain its own match. - ContractExecutor { Aot(AotContractExecutor), Emu(EmuContractInfo) }, with Emu gated on the new sierra-emu cargo feature. - EmuContractInfo carries Arc so the program is shared across invocations rather than cloned per call. - ContractExecutor::run dispatches: Aot delegates to AotContractExecutor::run; Emu constructs a sierra_emu::VirtualMachine and runs it with the caller's syscall handler directly. No adapter is needed -- the trait is shared via the cairo-starknet-syscalls crate. VirtualMachine::run's `Option` return is propagated as Error::UnexpectedValue rather than `.expect()`-aborted, matching the Aot arm's error-handling style. - Cargo: new `sierra-emu` feature (= ["dep:sierra-emu"]). The existing `with-trace-dump` feature now activates `sierra-emu` instead of the optional dep directly. Companion to the bridge-free design: with cairo-native and sierra-emu sharing one trait, the SierraEmuSyscallBridge that PR #1597 / #1607 introduced is no longer necessary. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 5 +- src/executor.rs | 8 +- src/executor/contract_executor.rs | 124 ++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/executor/contract_executor.rs diff --git a/Cargo.toml b/Cargo.toml index f0ee0a9fb..7c331633b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,10 @@ with-debug-utils = [] with-mem-tracing = [] with-libfunc-profiling = [] with-segfault-catcher = [] -with-trace-dump = ["dep:sierra-emu"] +with-trace-dump = ["sierra-emu"] +# Enables the `ContractExecutor::Emu` variant for dispatching contract execution +# through the sierra-emu interpreter. +sierra-emu = ["dep:sierra-emu"] testing = ["dep:cairo-lang-compiler", "dep:cairo-lang-filesystem"] [dependencies] diff --git a/src/executor.rs b/src/executor.rs index 9337c8e05..861bf8d23 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -3,7 +3,12 @@ //! This module provides methods to execute the programs, either via JIT or compiled ahead //! of time. It also provides a cache to avoid recompiling previously compiled programs. -pub use self::{aot::AotNativeExecutor, contract::AotContractExecutor, jit::JitNativeExecutor}; +#[cfg(feature = "sierra-emu")] +pub use self::contract_executor::EmuContractInfo; +pub use self::{ + aot::AotNativeExecutor, contract::AotContractExecutor, contract_executor::ContractExecutor, + jit::JitNativeExecutor, +}; use crate::{ arch::{AbiArgument, ValueWithInfoWrapper}, error::{panic::ToNativeAssertError, Error}, @@ -39,6 +44,7 @@ use std::{alloc::Layout, arch::global_asm, ptr::NonNull}; mod aot; mod contract; +mod contract_executor; mod jit; #[cfg(target_arch = "aarch64")] diff --git a/src/executor/contract_executor.rs b/src/executor/contract_executor.rs new file mode 100644 index 000000000..7bc31868a --- /dev/null +++ b/src/executor/contract_executor.rs @@ -0,0 +1,124 @@ +//! Dispatch enum that lets a single call site choose between AOT-compiled execution and +//! sierra-emu interpretation, without changing call-site code. +//! +//! The `Emu` variant is gated on the `sierra-emu` feature. Both variants share the same +//! `H: StarknetSyscallHandler` -- sierra-emu and cairo-native re-export the trait from +//! `cairo-native-syscalls`, so no adapter is needed. + +#[cfg(feature = "sierra-emu")] +use cairo_lang_sierra::program::Program; +#[cfg(feature = "sierra-emu")] +use cairo_lang_starknet_classes::compiler_version::VersionId; +#[cfg(feature = "sierra-emu")] +use cairo_lang_starknet_classes::contract_class::ContractEntryPoints; +use starknet_types_core::felt::Felt; +#[cfg(feature = "sierra-emu")] +use std::sync::Arc; + +#[cfg(feature = "sierra-emu")] +use crate::error::Error; +use crate::error::Result; +use crate::execution_result::ContractExecutionResult; +use crate::executor::AotContractExecutor; +use crate::starknet::StarknetSyscallHandler; +use crate::utils::BuiltinCosts; + +/// Runtime selection between cairo-native's AOT executor and the sierra-emu interpreter. +/// +/// `Emu` is constructed from the program + entry points + sierra version triple that +/// `sierra_emu::VirtualMachine::new_starknet` requires; the `Arc` is shared across +/// invocations rather than cloned per call. +#[derive(Debug)] +pub enum ContractExecutor { + Aot(AotContractExecutor), + #[cfg(feature = "sierra-emu")] + Emu(EmuContractInfo), +} + +/// Inputs required to construct a `sierra_emu::VirtualMachine` for the `Emu` variant. +#[cfg(feature = "sierra-emu")] +#[derive(Debug, Clone)] +pub struct EmuContractInfo { + pub program: Arc, + pub entry_points: ContractEntryPoints, + pub sierra_version: VersionId, +} + +impl From for ContractExecutor { + fn from(value: AotContractExecutor) -> Self { + Self::Aot(value) + } +} + +#[cfg(feature = "sierra-emu")] +impl From for ContractExecutor { + fn from(value: EmuContractInfo) -> Self { + Self::Emu(value) + } +} + +impl ContractExecutor { + /// Run the contract entry point identified by `selector`. + /// + /// Dispatches to [`AotContractExecutor::run`] for the `Aot` variant and to a + /// [`sierra_emu::VirtualMachine`] for the `Emu` variant. The same `syscall_handler` + /// flows through both paths unchanged -- its trait is shared across the two crates. + pub fn run( + &self, + selector: Felt, + args: &[Felt], + gas: u64, + builtin_costs: Option, + #[cfg_attr(not(feature = "sierra-emu"), allow(unused_mut))] mut syscall_handler: H, + ) -> Result { + match self { + ContractExecutor::Aot(aot) => { + aot.run(selector, args, gas, builtin_costs, syscall_handler) + } + #[cfg(feature = "sierra-emu")] + ContractExecutor::Emu(EmuContractInfo { + program, + entry_points, + sierra_version, + }) => { + let mut virtual_machine = sierra_emu::VirtualMachine::new_starknet( + Arc::clone(program), + entry_points, + *sierra_version, + ); + + let emu_builtin_costs = builtin_costs.map(convert_builtin_costs); + + virtual_machine.call_contract(selector, gas, args.to_vec(), emu_builtin_costs); + + // `VirtualMachine::run` returns `None` when the VM never produced a + // final state -- propagate as an error rather than aborting the host. + let result = virtual_machine.run(&mut syscall_handler).ok_or_else(|| { + Error::UnexpectedValue("sierra-emu VM produced no final state".to_string()) + })?; + + Ok(ContractExecutionResult { + remaining_gas: result.remaining_gas, + failure_flag: result.failure_flag, + return_values: result.return_values, + error_msg: result.error_msg, + builtin_stats: Default::default(), + }) + } + } + } +} + +#[cfg(feature = "sierra-emu")] +fn convert_builtin_costs(builtin_costs: BuiltinCosts) -> sierra_emu::BuiltinCosts { + sierra_emu::BuiltinCosts { + r#const: builtin_costs.r#const, + pedersen: builtin_costs.pedersen, + bitwise: builtin_costs.bitwise, + ecop: builtin_costs.ecop, + poseidon: builtin_costs.poseidon, + add_mod: builtin_costs.add_mod, + mul_mod: builtin_costs.mul_mod, + blake: builtin_costs.blake, + } +} From 0c045799b3edd1d105d9ad48fc62f1277f6feb6c Mon Sep 17 00:00:00 2001 From: Avi Cohen Date: Mon, 6 Jul 2026 13:37:16 +0300 Subject: [PATCH 2/3] move Emu-arm VM setup into sierra_emu::VirtualMachine::run_contract Co-Authored-By: Claude Fable 5 --- debug_utils/sierra-emu/src/vm.rs | 26 ++++++++++++++++++++++++++ src/executor/contract_executor.rs | 20 ++++++++++---------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/debug_utils/sierra-emu/src/vm.rs b/debug_utils/sierra-emu/src/vm.rs index 294cbc236..ed109b6d2 100644 --- a/debug_utils/sierra-emu/src/vm.rs +++ b/debug_utils/sierra-emu/src/vm.rs @@ -449,6 +449,32 @@ impl VirtualMachine { ContractExecutionResult::from_state(&last?) } + + /// One-shot convenience over [`VirtualMachine::new_starknet`], + /// [`VirtualMachine::call_contract`] and [`VirtualMachine::run`]: build a + /// Starknet VM for `program`, call the entry point identified by `selector` + /// and run it to completion. + /// + /// Returns `None` when execution never produced a final state. + #[allow(clippy::too_many_arguments)] + pub fn run_contract( + program: Arc, + entry_points: &ContractEntryPoints, + sierra_version: VersionId, + selector: Felt, + initial_gas: u64, + calldata: I, + builtin_costs: Option, + syscall_handler: &mut impl StarknetSyscallHandler, + ) -> Option + where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + { + let mut vm = Self::new_starknet(program, entry_points, sierra_version); + vm.call_contract(selector, initial_gas, calldata, builtin_costs); + vm.run(syscall_handler) + } } #[derive(Clone, Debug)] diff --git a/src/executor/contract_executor.rs b/src/executor/contract_executor.rs index 7bc31868a..8b0ff8a9c 100644 --- a/src/executor/contract_executor.rs +++ b/src/executor/contract_executor.rs @@ -81,19 +81,19 @@ impl ContractExecutor { entry_points, sierra_version, }) => { - let mut virtual_machine = sierra_emu::VirtualMachine::new_starknet( + // `run_contract` returns `None` when the VM never produced a + // final state -- propagate as an error rather than aborting the host. + let result = sierra_emu::VirtualMachine::run_contract( Arc::clone(program), entry_points, *sierra_version, - ); - - let emu_builtin_costs = builtin_costs.map(convert_builtin_costs); - - virtual_machine.call_contract(selector, gas, args.to_vec(), emu_builtin_costs); - - // `VirtualMachine::run` returns `None` when the VM never produced a - // final state -- propagate as an error rather than aborting the host. - let result = virtual_machine.run(&mut syscall_handler).ok_or_else(|| { + selector, + gas, + args.to_vec(), + builtin_costs.map(convert_builtin_costs), + &mut syscall_handler, + ) + .ok_or_else(|| { Error::UnexpectedValue("sierra-emu VM produced no final state".to_string()) })?; From 4fdd84188251a24664b9b926b203d1ceaaec3492 Mon Sep 17 00:00:00 2001 From: Avi Cohen Date: Mon, 6 Jul 2026 14:58:06 +0300 Subject: [PATCH 3/3] replace ContractExecutor enum with standalone EmuContractExecutor The dispatch enum is gone: consumers pick an executor type per build (feature flags) rather than per value. EmuContractInfo becomes EmuContractExecutor with an inherent run() that mirrors AotContractExecutor::run, so the two are interchangeable at call sites. Co-Authored-By: Claude Fable 5 --- src/executor.rs | 10 +-- src/executor/contract_executor.rs | 124 -------------------------- src/executor/emu_contract_executor.rs | 82 +++++++++++++++++ 3 files changed, 86 insertions(+), 130 deletions(-) delete mode 100644 src/executor/contract_executor.rs create mode 100644 src/executor/emu_contract_executor.rs diff --git a/src/executor.rs b/src/executor.rs index 861bf8d23..a7f0acfbc 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -4,11 +4,8 @@ //! of time. It also provides a cache to avoid recompiling previously compiled programs. #[cfg(feature = "sierra-emu")] -pub use self::contract_executor::EmuContractInfo; -pub use self::{ - aot::AotNativeExecutor, contract::AotContractExecutor, contract_executor::ContractExecutor, - jit::JitNativeExecutor, -}; +pub use self::emu_contract_executor::EmuContractExecutor; +pub use self::{aot::AotNativeExecutor, contract::AotContractExecutor, jit::JitNativeExecutor}; use crate::{ arch::{AbiArgument, ValueWithInfoWrapper}, error::{panic::ToNativeAssertError, Error}, @@ -44,7 +41,8 @@ use std::{alloc::Layout, arch::global_asm, ptr::NonNull}; mod aot; mod contract; -mod contract_executor; +#[cfg(feature = "sierra-emu")] +mod emu_contract_executor; mod jit; #[cfg(target_arch = "aarch64")] diff --git a/src/executor/contract_executor.rs b/src/executor/contract_executor.rs deleted file mode 100644 index 8b0ff8a9c..000000000 --- a/src/executor/contract_executor.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Dispatch enum that lets a single call site choose between AOT-compiled execution and -//! sierra-emu interpretation, without changing call-site code. -//! -//! The `Emu` variant is gated on the `sierra-emu` feature. Both variants share the same -//! `H: StarknetSyscallHandler` -- sierra-emu and cairo-native re-export the trait from -//! `cairo-native-syscalls`, so no adapter is needed. - -#[cfg(feature = "sierra-emu")] -use cairo_lang_sierra::program::Program; -#[cfg(feature = "sierra-emu")] -use cairo_lang_starknet_classes::compiler_version::VersionId; -#[cfg(feature = "sierra-emu")] -use cairo_lang_starknet_classes::contract_class::ContractEntryPoints; -use starknet_types_core::felt::Felt; -#[cfg(feature = "sierra-emu")] -use std::sync::Arc; - -#[cfg(feature = "sierra-emu")] -use crate::error::Error; -use crate::error::Result; -use crate::execution_result::ContractExecutionResult; -use crate::executor::AotContractExecutor; -use crate::starknet::StarknetSyscallHandler; -use crate::utils::BuiltinCosts; - -/// Runtime selection between cairo-native's AOT executor and the sierra-emu interpreter. -/// -/// `Emu` is constructed from the program + entry points + sierra version triple that -/// `sierra_emu::VirtualMachine::new_starknet` requires; the `Arc` is shared across -/// invocations rather than cloned per call. -#[derive(Debug)] -pub enum ContractExecutor { - Aot(AotContractExecutor), - #[cfg(feature = "sierra-emu")] - Emu(EmuContractInfo), -} - -/// Inputs required to construct a `sierra_emu::VirtualMachine` for the `Emu` variant. -#[cfg(feature = "sierra-emu")] -#[derive(Debug, Clone)] -pub struct EmuContractInfo { - pub program: Arc, - pub entry_points: ContractEntryPoints, - pub sierra_version: VersionId, -} - -impl From for ContractExecutor { - fn from(value: AotContractExecutor) -> Self { - Self::Aot(value) - } -} - -#[cfg(feature = "sierra-emu")] -impl From for ContractExecutor { - fn from(value: EmuContractInfo) -> Self { - Self::Emu(value) - } -} - -impl ContractExecutor { - /// Run the contract entry point identified by `selector`. - /// - /// Dispatches to [`AotContractExecutor::run`] for the `Aot` variant and to a - /// [`sierra_emu::VirtualMachine`] for the `Emu` variant. The same `syscall_handler` - /// flows through both paths unchanged -- its trait is shared across the two crates. - pub fn run( - &self, - selector: Felt, - args: &[Felt], - gas: u64, - builtin_costs: Option, - #[cfg_attr(not(feature = "sierra-emu"), allow(unused_mut))] mut syscall_handler: H, - ) -> Result { - match self { - ContractExecutor::Aot(aot) => { - aot.run(selector, args, gas, builtin_costs, syscall_handler) - } - #[cfg(feature = "sierra-emu")] - ContractExecutor::Emu(EmuContractInfo { - program, - entry_points, - sierra_version, - }) => { - // `run_contract` returns `None` when the VM never produced a - // final state -- propagate as an error rather than aborting the host. - let result = sierra_emu::VirtualMachine::run_contract( - Arc::clone(program), - entry_points, - *sierra_version, - selector, - gas, - args.to_vec(), - builtin_costs.map(convert_builtin_costs), - &mut syscall_handler, - ) - .ok_or_else(|| { - Error::UnexpectedValue("sierra-emu VM produced no final state".to_string()) - })?; - - Ok(ContractExecutionResult { - remaining_gas: result.remaining_gas, - failure_flag: result.failure_flag, - return_values: result.return_values, - error_msg: result.error_msg, - builtin_stats: Default::default(), - }) - } - } - } -} - -#[cfg(feature = "sierra-emu")] -fn convert_builtin_costs(builtin_costs: BuiltinCosts) -> sierra_emu::BuiltinCosts { - sierra_emu::BuiltinCosts { - r#const: builtin_costs.r#const, - pedersen: builtin_costs.pedersen, - bitwise: builtin_costs.bitwise, - ecop: builtin_costs.ecop, - poseidon: builtin_costs.poseidon, - add_mod: builtin_costs.add_mod, - mul_mod: builtin_costs.mul_mod, - blake: builtin_costs.blake, - } -} diff --git a/src/executor/emu_contract_executor.rs b/src/executor/emu_contract_executor.rs new file mode 100644 index 000000000..4622a652d --- /dev/null +++ b/src/executor/emu_contract_executor.rs @@ -0,0 +1,82 @@ +//! A sierra-emu-backed contract executor exposing the same `run` shape as +//! [`AotContractExecutor::run`](crate::executor::AotContractExecutor::run), so a caller +//! can swap the two behind a feature flag without changing call-site code. +//! +//! Both executors share the same `H: StarknetSyscallHandler` -- sierra-emu and +//! cairo-native re-export the trait from `cairo-native-syscalls`, so no adapter is +//! needed. + +use cairo_lang_sierra::program::Program; +use cairo_lang_starknet_classes::compiler_version::VersionId; +use cairo_lang_starknet_classes::contract_class::ContractEntryPoints; +use starknet_types_core::felt::Felt; +use std::sync::Arc; + +use crate::error::{Error, Result}; +use crate::execution_result::ContractExecutionResult; +use crate::starknet::StarknetSyscallHandler; +use crate::utils::BuiltinCosts; + +/// Runs contract entry points through the sierra-emu interpreter. +/// +/// Holds the program + entry points + sierra version triple that +/// `sierra_emu::VirtualMachine::run_contract` requires; the `Arc` is shared +/// across invocations rather than cloned per call. +#[derive(Debug, Clone)] +pub struct EmuContractExecutor { + pub program: Arc, + pub entry_points: ContractEntryPoints, + pub sierra_version: VersionId, +} + +impl EmuContractExecutor { + /// Run the contract entry point identified by `selector`. + /// + /// Mirrors [`AotContractExecutor::run`](crate::executor::AotContractExecutor::run) so + /// the two executor types are interchangeable at the call site. + pub fn run( + &self, + selector: Felt, + args: &[Felt], + gas: u64, + builtin_costs: Option, + mut syscall_handler: impl StarknetSyscallHandler, + ) -> Result { + // `run_contract` returns `None` when the VM never produced a final state -- + // propagate as an error rather than aborting the host. + let result = sierra_emu::VirtualMachine::run_contract( + Arc::clone(&self.program), + &self.entry_points, + self.sierra_version, + selector, + gas, + args.to_vec(), + builtin_costs.map(convert_builtin_costs), + &mut syscall_handler, + ) + .ok_or_else(|| { + Error::UnexpectedValue("sierra-emu VM produced no final state".to_string()) + })?; + + Ok(ContractExecutionResult { + remaining_gas: result.remaining_gas, + failure_flag: result.failure_flag, + return_values: result.return_values, + error_msg: result.error_msg, + builtin_stats: Default::default(), + }) + } +} + +fn convert_builtin_costs(builtin_costs: BuiltinCosts) -> sierra_emu::BuiltinCosts { + sierra_emu::BuiltinCosts { + r#const: builtin_costs.r#const, + pedersen: builtin_costs.pedersen, + bitwise: builtin_costs.bitwise, + ecop: builtin_costs.ecop, + poseidon: builtin_costs.poseidon, + add_mod: builtin_costs.add_mod, + mul_mod: builtin_costs.mul_mod, + blake: builtin_costs.blake, + } +}