diff --git a/Cargo.lock b/Cargo.lock index 81fcadcb1..0730eabe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2437,6 +2437,7 @@ dependencies = [ "pecos-quest", "pecos-qulacs", "pecos-rng", + "pecos-wasm", "serde_json", "tempfile", ] @@ -2598,15 +2599,14 @@ name = "pecos-phir-json" version = "0.1.1" dependencies = [ "log", - "parking_lot", "pecos-core", "pecos-engines", "pecos-phir", "pecos-programs", + "pecos-wasm", "serde", "serde_json", "tempfile", - "wasmtime", "wat", ] @@ -2626,12 +2626,12 @@ dependencies = [ "pecos-core", "pecos-engines", "pecos-programs", + "pecos-wasm", "pest", "pest_derive", "regex", "serde_json", "tempfile", - "wasmtime", "wat", ] @@ -2760,6 +2760,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "pecos-wasm" +version = "0.1.1" +dependencies = [ + "log", + "parking_lot", + "pecos-core", + "wasmtime", +] + [[package]] name = "percent-encoding" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index c631cdac7..088adc3d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,7 @@ pecos-qis-selene = { version = "0.1.1", path = "crates/pecos-qis-selene" } pecos-qis-core = { version = "0.1.1", path = "crates/pecos-qis-core" } pecos-hugr-qis = { version = "0.1.1", path = "crates/pecos-hugr-qis" } pecos-rslib = { version = "0.1.1", path = "python/pecos-rslib/rust" } +pecos-wasm = { version = "0.1.1", path = "crates/pecos-wasm" } pecos-build-utils = { version = "0.1.1", path = "crates/pecos-build-utils" } pecos-llvm-utils = { version = "0.1.1", path = "crates/pecos-llvm-utils" } diff --git a/crates/pecos-phir-json/Cargo.toml b/crates/pecos-phir-json/Cargo.toml index f50308b5d..c29e7d9ae 100644 --- a/crates/pecos-phir-json/Cargo.toml +++ b/crates/pecos-phir-json/Cargo.toml @@ -15,7 +15,7 @@ description = "PHIR-JSON (PECOS High-level Intermediate Representation JSON form default = ["v0_1", "wasm"] v0_1 = [] all-versions = ["v0_1"] -wasm = ["wasmtime", "wat", "parking_lot"] +wasm = ["pecos-wasm/wasm", "wat"] [dependencies] log.workspace = true @@ -25,9 +25,8 @@ pecos-core.workspace = true pecos-engines.workspace = true pecos-phir.workspace = true pecos-programs.workspace = true -wasmtime = { workspace = true, optional = true } +pecos-wasm = { workspace = true, optional = true } wat = { workspace = true, optional = true } -parking_lot = { workspace = true, optional = true } [dev-dependencies] # Testing diff --git a/crates/pecos-phir-json/src/v0_1/foreign_objects.rs b/crates/pecos-phir-json/src/v0_1/foreign_objects.rs index b50929563..62f6bf887 100644 --- a/crates/pecos-phir-json/src/v0_1/foreign_objects.rs +++ b/crates/pecos-phir-json/src/v0_1/foreign_objects.rs @@ -1,88 +1,7 @@ -use pecos_core::errors::PecosError; -use std::any::Any; -use std::fmt::Debug; +// Re-export from pecos-wasm crate +#[cfg(feature = "wasm")] +pub use pecos_wasm::{DummyForeignObject, ForeignObject}; -/// Trait for foreign object implementations -pub trait ForeignObject: Debug + Send + Sync { - /// Clone the foreign object - fn clone_box(&self) -> Box; - /// Initialize object before running a series of simulations - /// - /// # Errors - /// Returns an error if initialization fails. - fn init(&mut self) -> Result<(), PecosError>; - - /// Create new instance/internal state - /// - /// # Errors - /// Returns an error if instance creation fails. - fn new_instance(&mut self) -> Result<(), PecosError>; - - /// Get a list of function names available from the object - fn get_funcs(&self) -> Vec; - - /// Execute a function given a list of arguments - /// - /// # Errors - /// Returns an error if the function does not exist or execution fails. - fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError>; - - /// Cleanup resources - fn teardown(&mut self) {} - - /// Get as Any for downcasting - fn as_any(&self) -> &dyn Any; - - /// Get as Any for downcasting (mutable) - fn as_any_mut(&mut self) -> &mut dyn Any; -} - -/// Dummy foreign object for when no foreign object is needed -#[derive(Debug, Clone)] -pub struct DummyForeignObject {} - -impl DummyForeignObject { - /// Create a new dummy foreign object - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for DummyForeignObject { - fn default() -> Self { - Self::new() - } -} - -impl ForeignObject for DummyForeignObject { - fn clone_box(&self) -> Box { - Box::new(Self::default()) - } - - fn init(&mut self) -> Result<(), PecosError> { - Ok(()) - } - - fn new_instance(&mut self) -> Result<(), PecosError> { - Ok(()) - } - - fn get_funcs(&self) -> Vec { - vec![] - } - - fn exec(&mut self, func_name: &str, _args: &[i64]) -> Result, PecosError> { - Err(PecosError::Input(format!( - "Dummy foreign object cannot execute function: {func_name}" - ))) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} +// For when wasm feature is disabled, provide minimal trait +#[cfg(not(feature = "wasm"))] +pub use pecos_wasm::{DummyForeignObject, ForeignObject}; diff --git a/crates/pecos-phir-json/src/v0_1/wasm_foreign_object.rs b/crates/pecos-phir-json/src/v0_1/wasm_foreign_object.rs index 071816417..d06ed51a7 100644 --- a/crates/pecos-phir-json/src/v0_1/wasm_foreign_object.rs +++ b/crates/pecos-phir-json/src/v0_1/wasm_foreign_object.rs @@ -1,351 +1,19 @@ -#[cfg(feature = "wasm")] -use crate::v0_1::foreign_objects::ForeignObject; -#[cfg(feature = "wasm")] -use log::{debug, warn}; -#[cfg(feature = "wasm")] -use parking_lot::{Mutex, RwLock}; -#[cfg(feature = "wasm")] -use pecos_core::errors::PecosError; -#[cfg(feature = "wasm")] -use std::any::Any; -#[cfg(feature = "wasm")] -use std::path::Path; -#[cfg(feature = "wasm")] -use std::sync::Arc; -#[cfg(feature = "wasm")] -use std::thread; -#[cfg(feature = "wasm")] -use std::time::Duration; -#[cfg(feature = "wasm")] -use wasmtime::{Config, Engine, Func, Instance, Module, Store, Trap, Val}; - -#[cfg(feature = "wasm")] -const WASM_EXECUTION_MAX_TICKS: u64 = 10_000; -#[cfg(feature = "wasm")] -const WASM_EXECUTION_TICK_LENGTH_MS: u64 = 10; - -/// WebAssembly foreign object implementation for executing WebAssembly functions -#[cfg(feature = "wasm")] -#[derive(Debug)] -pub struct WasmtimeForeignObject { - /// WebAssembly binary - #[allow(dead_code)] - wasm_bytes: Vec, - /// Wasmtime engine - #[allow(dead_code)] - engine: Engine, - /// Wasmtime module - module: Module, - /// Wasmtime store - store: RwLock>, - /// Wasmtime instance - instance: RwLock>, - /// Available functions - func_names: Mutex>>, - /// Timeout flag for long-running operations - stop_flag: Arc>, - /// Last function call results - last_results: Vec, -} - -#[cfg(feature = "wasm")] -impl WasmtimeForeignObject { - /// Create a new WebAssembly foreign object from a file - /// - /// # Parameters - /// - /// * `path` - Path to the WebAssembly file (.wasm or .wat) - /// - /// # Returns - /// - /// A new WebAssembly foreign object - /// - /// # Errors - /// - /// Returns an error if the file cannot be read or if WebAssembly compilation fails - pub fn new>(path: P) -> Result { - // Read the WebAssembly file - let wasm_bytes = std::fs::read(path) - .map_err(|e| PecosError::Input(format!("Failed to read WebAssembly file: {e}")))?; - - Self::from_bytes(&wasm_bytes) - } - - /// Create a new WebAssembly foreign object from bytes - /// - /// # Parameters - /// - /// * `wasm_bytes` - WebAssembly binary - /// - /// # Returns - /// - /// A new WebAssembly foreign object - /// - /// # Errors - /// - /// Returns an error if WebAssembly compilation fails - pub fn from_bytes(wasm_bytes: &[u8]) -> Result { - // Create a new WebAssembly engine - let mut config = Config::new(); - config.epoch_interruption(true); - let engine = Engine::new(&config).map_err(|e| { - PecosError::Processing(format!("Failed to create WebAssembly engine: {e}")) - })?; - - // Create a new store - let store = Store::new(&engine, ()); - - // Compile the WebAssembly module - let module = Module::new(&engine, wasm_bytes).map_err(|e| { - PecosError::Processing(format!("Failed to compile WebAssembly module: {e}")) - })?; - - let stop_flag = Arc::new(RwLock::new(false)); - let engine_clone = engine.clone(); - let stop_flag_clone = stop_flag.clone(); - - // Start the epoch increment thread - thread::spawn(move || { - while !*stop_flag_clone.read() { - // Increment the epoch every tick length - engine_clone.increment_epoch(); - thread::sleep(Duration::from_millis(WASM_EXECUTION_TICK_LENGTH_MS)); - } - }); - - let mut foreign_object = Self { - wasm_bytes: wasm_bytes.to_vec(), - engine, - module, - store: RwLock::new(store), - instance: RwLock::new(None), - func_names: Mutex::new(None), - stop_flag, - last_results: Vec::new(), - }; - - // Create the instance - foreign_object.new_instance()?; - - Ok(foreign_object) - } - - /// Get a function from the WebAssembly instance - /// - /// # Parameters - /// - /// * `func_name` - Name of the function to get - /// - /// # Returns - /// - /// The WebAssembly function - /// - /// # Errors - /// - /// Returns an error if the function is not found - fn get_function(&self, func_name: &str) -> Result { - // Get the instance - let instance = self.instance.read(); - let instance = instance - .as_ref() - .ok_or_else(|| PecosError::Resource("WebAssembly instance not created".to_string()))?; - - // Get the function - let mut store = self.store.write(); - let func = instance.get_func(&mut *store, func_name).ok_or_else(|| { - PecosError::Resource(format!("WebAssembly function '{func_name}' not found")) - })?; - - Ok(func) - } -} - -#[cfg(feature = "wasm")] -impl ForeignObject for WasmtimeForeignObject { - fn clone_box(&self) -> Box { - // Create a new instance from the same bytes - let mut result = - Self::from_bytes(&self.wasm_bytes).expect("Failed to clone WasmtimeForeignObject"); - - // Initialize it the same way - if self.instance.read().is_some() { - let _ = result.new_instance(); - } - - Box::new(result) - } - - fn init(&mut self) -> Result<(), PecosError> { - // Create a new instance - self.new_instance()?; - - // Check if the init function exists - let funcs = self.get_funcs(); - if !funcs.contains(&"init".to_string()) { - return Err(PecosError::Input( - "WebAssembly module must contain an 'init' function".to_string(), - )); - } - - // Call the init function - self.exec("init", &[])?; - - Ok(()) - } - - fn new_instance(&mut self) -> Result<(), PecosError> { - let mut store = self.store.write(); - - // Create a new instance - let instance = Instance::new(&mut *store, &self.module, &[]).map_err(|e| { - PecosError::Processing(format!("Failed to create WebAssembly instance: {e}")) - })?; - - // Store the instance - *self.instance.write() = Some(instance); - - Ok(()) - } - - fn get_funcs(&self) -> Vec { - // Check if we've already cached the function names - if let Some(ref funcs) = *self.func_names.lock() { - return funcs.clone(); - } - - // Get the function names - let mut funcs = Vec::new(); - for export in self.module.exports() { - if export.ty().func().is_some() { - funcs.push(export.name().to_string()); - } - } - - // Cache the function names - *self.func_names.lock() = Some(funcs.clone()); - - funcs - } - - fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError> { - debug!("Executing WebAssembly function '{func_name}' with args {args:?}"); - - // Get the function - let func = self.get_function(func_name)?; - - // Convert the arguments - let wasm_args: Vec<_> = args - .iter() - .map(|a| { - // Try to convert i64 to i32 with proper bounds checking - let value = if *a > i64::from(i32::MAX) { - warn!("Argument value {a} exceeds i32::MAX, clamping to i32::MAX"); - i32::MAX - } else if *a < i64::from(i32::MIN) { - warn!("Argument value {a} is less than i32::MIN, clamping to i32::MIN"); - i32::MIN - } else { - // Safe: we've verified the value is in range - i32::try_from(*a).expect("Value should be in range after bounds check") - }; - wasmtime::Val::I32(value) - }) - .collect(); - - // Execute the function - let mut store = self.store.write(); - store.set_epoch_deadline(WASM_EXECUTION_MAX_TICKS); - - // Get the function type to determine the number of results - let func_type = func.ty(&*store); - let results_len = func_type.results().len(); - - // Handle functions based on their return type - let result = if results_len == 0 { - // Function returns nothing (like init) - func.call(&mut *store, &wasm_args, &mut []) - } else { - // Function returns something, create an appropriate buffer - let mut results_buffer = vec![Val::I32(0); results_len]; - debug!( - "Calling WebAssembly function '{func_name}' with args {wasm_args:?}, expecting {results_len} results" - ); - let res = func.call(&mut *store, &wasm_args, &mut results_buffer); - - // Store the results if successful - if res.is_ok() { - debug!("WebAssembly function returned {results_buffer:?}"); - self.last_results = results_buffer; - } - - res - }; - - // Handle the result - match result { - Ok(()) => { - if results_len == 0 { - // Functions with no return value - Ok(vec![0]) - } else { - // Convert the results back to i64 - let results: Vec = self - .last_results - .iter() - .map(|r| match r { - Val::I32(val) => i64::from(*val), - Val::I64(val) => *val, - _ => { - warn!("Unexpected result type from WebAssembly function"); - 0 - } - }) - .collect(); - - if results.is_empty() { - // If there are no results, return a zero - Ok(vec![0]) - } else { - Ok(results) - } - } - } - Err(e) => { - // Check if the error is a timeout - if let Some(trap) = e.downcast_ref::() - && trap.to_string().contains("interrupt") - { - let timeout_ms = WASM_EXECUTION_MAX_TICKS * WASM_EXECUTION_TICK_LENGTH_MS; - return Err(PecosError::Processing(format!( - "WebAssembly function '{func_name}' timed out after {timeout_ms}ms" - ))); - } - - Err(PecosError::Processing(format!( - "WebAssembly function '{func_name}' failed with error: {e}" - ))) - } - } - } - - fn teardown(&mut self) { - // Set the stop flag to stop the epoch increment thread - *self.stop_flag.write() = true; - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -#[cfg(feature = "wasm")] -impl Drop for WasmtimeForeignObject { - fn drop(&mut self) { - // Set the stop flag to stop the epoch increment thread - *self.stop_flag.write() = true; - } -} +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! Re-export WebAssembly foreign object from pecos-wasm crate +//! +//! This module previously contained the `WasmtimeForeignObject` implementation, +//! but it has been moved to the unified pecos-wasm crate to avoid duplication +//! across different PECOS crates. + +#[cfg(feature = "wasm")] +pub use pecos_wasm::WasmForeignObject as WasmtimeForeignObject; diff --git a/crates/pecos-qasm/Cargo.toml b/crates/pecos-qasm/Cargo.toml index 8d9c247e5..67cf5d79e 100644 --- a/crates/pecos-qasm/Cargo.toml +++ b/crates/pecos-qasm/Cargo.toml @@ -13,7 +13,7 @@ description = "QASM parser and engine for PECOS quantum simulator" [features] default = ["wasm"] -wasm = ["wasmtime", "wat"] +wasm = ["pecos-wasm/wasm", "wat"] [dependencies] # Parser generator @@ -38,7 +38,7 @@ serde_json.workspace = true bitvec.workspace = true # Optional WebAssembly support -wasmtime = { workspace = true, optional = true } +pecos-wasm = { workspace = true, optional = true } wat = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/pecos-qasm/src/foreign_objects.rs b/crates/pecos-qasm/src/foreign_objects.rs index 7ed91173c..ce6f6f3e6 100644 --- a/crates/pecos-qasm/src/foreign_objects.rs +++ b/crates/pecos-qasm/src/foreign_objects.rs @@ -1,64 +1,23 @@ -use pecos_core::errors::PecosError; -use std::fmt::Debug; - -/// Trait for foreign object implementations in QASM -pub trait ForeignObject: Debug + Send + Sync { - /// Clone the foreign object - fn clone_box(&self) -> Box; - - /// Initialize object before running a series of simulations - /// - /// # Errors - /// Returns an error if initialization fails. - fn init(&mut self) -> Result<(), PecosError>; - - /// Create new instance/internal state for a new shot - /// - /// # Errors - /// Returns an error if instance creation fails. - fn new_instance(&mut self) -> Result<(), PecosError>; - - /// Execute a function given a list of arguments - /// - /// # Errors - /// Returns an error if the function does not exist or execution fails. - fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError>; -} - -/// Dummy foreign object for when no foreign object is needed -#[derive(Debug, Clone)] -pub struct DummyForeignObject {} - -impl DummyForeignObject { - /// Create a new dummy foreign object - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for DummyForeignObject { - fn default() -> Self { - Self::new() - } -} - -impl ForeignObject for DummyForeignObject { - fn clone_box(&self) -> Box { - Box::new(Self::default()) - } - - fn init(&mut self) -> Result<(), PecosError> { - Ok(()) - } - - fn new_instance(&mut self) -> Result<(), PecosError> { - Ok(()) - } - - fn exec(&mut self, func_name: &str, _args: &[i64]) -> Result, PecosError> { - Err(PecosError::Input(format!( - "Dummy foreign object cannot execute function: {func_name}" - ))) - } -} +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! Re-export foreign object trait from pecos-wasm crate +//! +//! This module previously defined its own `ForeignObject` trait, +//! but now uses the unified trait from pecos-wasm. + +// Re-export from pecos-wasm crate +#[cfg(feature = "wasm")] +pub use pecos_wasm::{DummyForeignObject, ForeignObject}; + +// For when wasm feature is disabled, provide minimal trait +#[cfg(not(feature = "wasm"))] +pub use pecos_wasm::{DummyForeignObject, ForeignObject}; diff --git a/crates/pecos-qasm/src/unified_engine_builder.rs b/crates/pecos-qasm/src/unified_engine_builder.rs index 520aa7361..a1c985a02 100644 --- a/crates/pecos-qasm/src/unified_engine_builder.rs +++ b/crates/pecos-qasm/src/unified_engine_builder.rs @@ -242,7 +242,7 @@ impl ClassicalControlEngineBuilder for QasmEngineBuilder { let wasm_obj = WasmtimeForeignObject::from_bytes(&wasm_program.wasm_bytes)?; // Get exported functions from WASM module - let exported_functions = wasm_obj.get_exported_functions(); + let exported_functions = wasm_obj.get_funcs(); // Check if init function exists if !exported_functions.contains(&"init".to_string()) { diff --git a/crates/pecos-qasm/src/wasm_foreign_object.rs b/crates/pecos-qasm/src/wasm_foreign_object.rs index 40ab4eb96..1f66b2f42 100644 --- a/crates/pecos-qasm/src/wasm_foreign_object.rs +++ b/crates/pecos-qasm/src/wasm_foreign_object.rs @@ -66,262 +66,22 @@ //! All function calls are validated at build time to ensure they exist in the WASM module. //! This eliminates runtime errors for missing functions. -#[cfg(feature = "wasm")] -use crate::foreign_objects::ForeignObject; -#[cfg(feature = "wasm")] -use log::debug; -#[cfg(feature = "wasm")] -use pecos_core::errors::PecosError; -#[cfg(feature = "wasm")] -use std::collections::BTreeMap; -#[cfg(feature = "wasm")] -use std::path::Path; -#[cfg(feature = "wasm")] -use wasmtime::{Engine, Func, Instance, Module, Store, Val}; - -/// WebAssembly foreign object implementation for executing WebAssembly functions -/// -/// Note: This implementation assumes that all function validation has been done -/// at build time. Function lookups use `expect()` instead of error handling -/// because we've already validated that: -/// 1. The WASM module exports an 'init' function -/// 2. All functions called from QASM exist in the WASM module -#[cfg(feature = "wasm")] -#[derive(Debug)] -pub struct WasmtimeForeignObject { - /// WebAssembly binary - #[allow(dead_code)] - wasm_bytes: Vec, - /// Wasmtime engine - #[allow(dead_code)] - engine: Engine, - /// Wasmtime module - module: Module, - /// Wasmtime store - store: Store<()>, - /// Wasmtime instance - instance: Option, - /// Cached function references - function_cache: BTreeMap, -} - -#[cfg(feature = "wasm")] -impl WasmtimeForeignObject { - /// Create a new WebAssembly foreign object from a file - /// - /// # Parameters - /// - /// * `path` - Path to the WebAssembly file (.wasm or .wat) - /// - /// # Returns - /// - /// A new WebAssembly foreign object - /// - /// # Errors - /// - /// Returns an error if the file cannot be read or if WebAssembly compilation fails - pub fn new>(path: P) -> Result { - // Read the WebAssembly file - let wasm_bytes = std::fs::read(path) - .map_err(|e| PecosError::Input(format!("Failed to read WebAssembly file: {e}")))?; - - Self::from_bytes(&wasm_bytes) - } - - /// Create a new WebAssembly foreign object from bytes - /// - /// # Parameters - /// - /// * `wasm_bytes` - WebAssembly binary - /// - /// # Returns - /// - /// A new WebAssembly foreign object - /// - /// # Errors - /// - /// Returns an error if WebAssembly compilation fails - pub fn from_bytes(wasm_bytes: &[u8]) -> Result { - // Create a new WebAssembly engine - let engine = Engine::default(); - - // Create a new store - let store = Store::new(&engine, ()); - - // Compile the WebAssembly module - let module = Module::new(&engine, wasm_bytes).map_err(|e| { - PecosError::Processing(format!("Failed to compile WebAssembly module: {e}")) - })?; - - Ok(Self { - wasm_bytes: wasm_bytes.to_vec(), - engine, - module, - store, - instance: None, - function_cache: BTreeMap::new(), - }) - } - - /// Get the list of exported function names from the module - #[must_use] - pub fn get_exported_functions(&self) -> Vec { - let mut functions = Vec::new(); - for export in self.module.exports() { - if matches!(export.ty(), wasmtime::ExternType::Func(_)) { - functions.push(export.name().to_string()); - } - } - functions - } - - /// Convert i64 to i32 with bounds checking - fn i64_to_i32(value: i64) -> Result { - if value > i64::from(i32::MAX) || value < i64::from(i32::MIN) { - Err(PecosError::Input(format!( - "Value {value} is out of range for i32" - ))) - } else { - #[allow(clippy::cast_possible_truncation)] - Ok(value as i32) - } - } -} +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! Re-export WebAssembly foreign object from pecos-wasm crate +//! +//! This module previously contained the `WasmtimeForeignObject` implementation, +//! but it has been moved to the unified pecos-wasm crate to avoid duplication +//! across different PECOS crates. #[cfg(feature = "wasm")] -impl ForeignObject for WasmtimeForeignObject { - fn clone_box(&self) -> Box { - Box::new(Self { - wasm_bytes: self.wasm_bytes.clone(), - engine: self.engine.clone(), - module: self.module.clone(), - store: Store::new(&self.engine, ()), - instance: None, - function_cache: BTreeMap::new(), - }) - } - - fn init(&mut self) -> Result<(), PecosError> { - // Create a new instance - let instance = Instance::new(&mut self.store, &self.module, &[]) - .map_err(|e| PecosError::Processing(format!("WASM instantiation failed: {e}")))?; - - // Get the init function (we already validated it exists at build time) - let init_func = instance - .get_func(&mut self.store, "init") - .expect("init function should exist (validated at build time)"); - - self.instance = Some(instance); - - // Clear the function cache (will be populated on first use) - self.function_cache.clear(); - - // Call init - match init_func.call(&mut self.store, &[], &mut []) { - Ok(()) => { - debug!("WebAssembly init function called successfully"); - Ok(()) - } - Err(e) => Err(PecosError::Processing(format!( - "WebAssembly function 'init' failed: {e}" - ))), - } - } - - fn new_instance(&mut self) -> Result<(), PecosError> { - // For QASM, we'll call init() at the start of each shot - // If no instance exists yet, do full initialization - if self.instance.is_none() { - return self.init(); - } - - // Otherwise just call the init function to reset state - let instance = self.instance.as_ref().expect("instance should exist"); - let init_func = instance - .get_func(&mut self.store, "init") - .expect("init function should exist (validated at build time)"); - - init_func - .call(&mut self.store, &[], &mut []) - .map_err(|e| PecosError::Processing(format!("WebAssembly function 'init' failed: {e}"))) - } - - fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError> { - let instance = self.instance.as_ref().ok_or_else(|| { - PecosError::Processing("WebAssembly instance not initialized".to_string()) - })?; - - // Get the function from cache or fetch and cache it - let func = if let Some(cached_func) = self.function_cache.get(func_name) { - *cached_func - } else { - // Get the function (we already validated it exists at build time) - let func = instance - .get_func(&mut self.store, func_name) - .unwrap_or_else(|| { - panic!("Function '{func_name}' should exist (validated at build time)") - }); - self.function_cache.insert(func_name.to_string(), func); - func - }; - - // Get function type - let func_ty = func.ty(&self.store); - let params = func_ty.params(); - let results = func_ty.results(); - - // Check parameter count - if params.len() != args.len() { - return Err(PecosError::Processing(format!( - "Function '{func_name}' expects {} arguments, got {}", - params.len(), - args.len() - ))); - } - - // Convert arguments - let mut wasm_args = Vec::new(); - for (i, (param_ty, &arg)) in params.zip(args.iter()).enumerate() { - match param_ty { - wasmtime::ValType::I32 => { - let val = Self::i64_to_i32(arg)?; - wasm_args.push(Val::I32(val)); - } - wasmtime::ValType::I64 => { - wasm_args.push(Val::I64(arg)); - } - _ => { - return Err(PecosError::Processing(format!( - "Unsupported parameter type for argument {i} of function '{func_name}'" - ))); - } - } - } - - // Prepare result buffer - let mut wasm_results = vec![Val::I32(0); results.len()]; - - // Call the function - match func.call(&mut self.store, &wasm_args, &mut wasm_results) { - Ok(()) => { - // Convert results to i64 - let mut results_i64 = Vec::new(); - for (i, val) in wasm_results.iter().enumerate() { - match val { - Val::I32(v) => results_i64.push(i64::from(*v)), - Val::I64(v) => results_i64.push(*v), - _ => { - return Err(PecosError::Processing(format!( - "Unsupported return type for result {i} of function '{func_name}'" - ))); - } - } - } - Ok(results_i64) - } - Err(e) => Err(PecosError::Processing(format!( - "WebAssembly function '{func_name}' failed: {e}" - ))), - } - } -} +pub use pecos_wasm::WasmForeignObject as WasmtimeForeignObject; diff --git a/crates/pecos-wasm/Cargo.toml b/crates/pecos-wasm/Cargo.toml new file mode 100644 index 000000000..5567a446a --- /dev/null +++ b/crates/pecos-wasm/Cargo.toml @@ -0,0 +1,38 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +[package] +name = "pecos-wasm" +version.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +readme = "README.md" +description = "WebAssembly foreign object support for PECOS" + +[dependencies] +pecos-core.workspace = true +log.workspace = true +parking_lot.workspace = true + +# Wasmtime dependencies (optional, enabled with "wasm" feature) +wasmtime = { workspace = true, optional = true } + +[features] +default = ["wasm"] +wasm = ["dep:wasmtime"] + +[lints] +workspace = true diff --git a/crates/pecos-wasm/README.md b/crates/pecos-wasm/README.md new file mode 100644 index 000000000..4ad70ac70 --- /dev/null +++ b/crates/pecos-wasm/README.md @@ -0,0 +1,19 @@ +# pecos-wasm + +`pecos-wasm` provides WebAssembly foreign object support for PECOS. + +This crate enables execution of WebAssembly modules for classical computations within the PECOS quantum error correction framework, with configurable timeout and memory limits. + +## Features + +- Thread-safe WASM execution via Wasmtime +- Configurable execution timeouts (default: 1 second) +- Configurable memory limits (default: unlimited) +- Support for both .wasm and .wat files + +## Usage + +This is an **internal crate** used by: +- `pecos-qasm` - QASM program execution with WASM foreign objects +- `pecos-phir-json` - PHIR program execution with WASM foreign objects +- `pecos-rslib` - Python bindings exposing WASM functionality diff --git a/crates/pecos-wasm/src/foreign_object.rs b/crates/pecos-wasm/src/foreign_object.rs new file mode 100644 index 000000000..6cf1e6f37 --- /dev/null +++ b/crates/pecos-wasm/src/foreign_object.rs @@ -0,0 +1,160 @@ +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! Unified `ForeignObject` trait for PECOS +//! +//! This trait defines the interface for foreign object implementations (like WebAssembly modules) +//! that can be called from PECOS quantum simulations. + +use pecos_core::errors::PecosError; +use std::any::Any; +use std::fmt::Debug; + +/// Trait for foreign object implementations +/// +/// This trait provides a unified interface for foreign objects (like WebAssembly modules) +/// that can be executed from quantum simulation programs. Implementations must be thread-safe +/// (`Send + Sync`) to support parallel execution. +/// +/// # Required Methods +/// +/// - `clone_box`: Create a boxed clone of the object +/// - `init`: Initialize the object before a series of simulations +/// - `new_instance`: Create a new instance/reset internal state +/// - `get_funcs`: Get list of available function names +/// - `exec`: Execute a named function with arguments +/// +/// # Optional Methods +/// +/// - `teardown`: Cleanup resources (default: no-op) +/// - `as_any`: Downcast to concrete type (for type inspection) +/// - `as_any_mut`: Mutable downcast to concrete type +pub trait ForeignObject: Debug + Send + Sync { + /// Clone the foreign object + /// + /// Returns a boxed clone that can be used independently. + fn clone_box(&self) -> Box; + + /// Initialize object before running a series of simulations + /// + /// This is typically called once before running multiple shots. It should: + /// - Create a new instance + /// - Call the `init` function in the foreign object if it exists + /// + /// # Errors + /// + /// Returns an error if initialization fails. + fn init(&mut self) -> Result<(), PecosError>; + + /// Create new instance/internal state + /// + /// This resets the internal state of the foreign object, typically called + /// at the start of each simulation shot. + /// + /// # Errors + /// + /// Returns an error if instance creation fails. + fn new_instance(&mut self) -> Result<(), PecosError>; + + /// Get a list of function names available from the object + /// + /// Returns all exported function names that can be called via `exec()`. + fn get_funcs(&self) -> Vec; + + /// Execute a function given a list of arguments + /// + /// # Parameters + /// + /// - `func_name`: Name of the function to execute + /// - `args`: Slice of i64 arguments to pass to the function + /// + /// # Returns + /// + /// Vector of i64 return values from the function + /// + /// # Errors + /// + /// Returns an error if: + /// - The function does not exist + /// - Execution fails + /// - Timeout occurs (if supported) + fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError>; + + /// Cleanup resources + /// + /// Called when the foreign object is no longer needed. Default implementation + /// does nothing, but implementations with background threads or other resources + /// should override this. + fn teardown(&mut self) {} + + /// Get as Any for downcasting + /// + /// Allows downcasting to the concrete type for type-specific operations. + fn as_any(&self) -> &dyn Any; + + /// Get as Any for downcasting (mutable) + /// + /// Allows mutable downcasting to the concrete type. + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +/// Dummy foreign object for when no foreign object is needed +/// +/// This is a no-op implementation that returns errors for all `exec()` calls. +/// Useful as a placeholder or default value. +#[derive(Debug, Clone)] +pub struct DummyForeignObject {} + +impl DummyForeignObject { + /// Create a new dummy foreign object + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for DummyForeignObject { + fn default() -> Self { + Self::new() + } +} + +impl ForeignObject for DummyForeignObject { + fn clone_box(&self) -> Box { + Box::new(Self::default()) + } + + fn init(&mut self) -> Result<(), PecosError> { + Ok(()) + } + + fn new_instance(&mut self) -> Result<(), PecosError> { + Ok(()) + } + + fn get_funcs(&self) -> Vec { + vec![] + } + + fn exec(&mut self, func_name: &str, _args: &[i64]) -> Result, PecosError> { + Err(PecosError::Input(format!( + "Dummy foreign object cannot execute function: {func_name}" + ))) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} diff --git a/crates/pecos-wasm/src/lib.rs b/crates/pecos-wasm/src/lib.rs new file mode 100644 index 000000000..edae4fd22 --- /dev/null +++ b/crates/pecos-wasm/src/lib.rs @@ -0,0 +1,50 @@ +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! WebAssembly foreign object support for PECOS +//! +//! This crate provides a unified WebAssembly foreign object implementation that can be used +//! across different PECOS crates (pecos-qasm, pecos-phir-json, etc.) and exposed to Python +//! via `PyO3`. +//! +//! # Features +//! +//! - Thread-safe execution with RwLock/Mutex synchronization +//! - Configurable timeout support via epoch interruption +//! - Type conversion between i32/i64 with bounds checking +//! - Function discovery and caching +//! - Shot reinitialization support +//! - Resource cleanup via Drop trait +//! +//! # Example +//! +//! ```no_run +//! # #[cfg(feature = "wasm")] { +//! use pecos_wasm::{WasmForeignObject, ForeignObject}; +//! +//! let mut wasm = WasmForeignObject::new("module.wasm").unwrap(); +//! wasm.init().unwrap(); +//! +//! let result = wasm.exec("add", &[5, 3]).unwrap(); +//! println!("Result: {:?}", result); +//! # } +//! ``` + +pub mod foreign_object; + +#[cfg(feature = "wasm")] +pub mod wasmtime_foreign_object; + +// Re-export main types +pub use foreign_object::{DummyForeignObject, ForeignObject}; + +#[cfg(feature = "wasm")] +pub use wasmtime_foreign_object::WasmForeignObject; diff --git a/crates/pecos-wasm/src/wasmtime_foreign_object.rs b/crates/pecos-wasm/src/wasmtime_foreign_object.rs new file mode 100644 index 000000000..5de2049c7 --- /dev/null +++ b/crates/pecos-wasm/src/wasmtime_foreign_object.rs @@ -0,0 +1,584 @@ +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +//! Wasmtime-based WebAssembly foreign object implementation +//! +//! This module provides a thread-safe, production-ready WebAssembly foreign object +//! implementation using the Wasmtime runtime. It supports timeout handling, proper +//! resource cleanup, and type conversions. + +use crate::foreign_object::ForeignObject; +use log::{debug, warn}; +use parking_lot::{Mutex, RwLock}; +use pecos_core::errors::PecosError; +use std::any::Any; +use std::path::Path; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use wasmtime::{ + Config, Engine, Func, Instance, Module, Store, StoreLimits, StoreLimitsBuilder, Trap, Val, +}; + +/// Length of each tick in milliseconds (10ms per tick) +const WASM_EXECUTION_TICK_LENGTH_MS: u64 = 10; +/// Default timeout in seconds (1 second to match Python implementation) +const DEFAULT_TIMEOUT_SECONDS: f64 = 1.0; + +/// Store context holding resource limits +#[derive(Debug)] +struct StoreContext { + limits: StoreLimits, +} + +impl StoreContext { + fn new(memory_size: Option) -> Self { + let mut builder = StoreLimitsBuilder::new(); + if let Some(size) = memory_size { + builder = builder.memory_size(size); + } + Self { + limits: builder.build(), + } + } +} + +/// WebAssembly foreign object implementation using Wasmtime +/// +/// This implementation provides: +/// - Thread-safe execution with RwLock/Mutex synchronization +/// - Configurable timeout via epoch interruption (default: 1 second) +/// - Configurable memory limits (default: unlimited) +/// - Type conversion between i32/i64 with bounds checking and warnings +/// - Function discovery and caching +/// - Proper resource cleanup via Drop trait +/// +/// # Example +/// +/// ```no_run +/// # use pecos_wasm::{WasmForeignObject, ForeignObject}; +/// // Create with defaults (1-second timeout, unlimited memory) +/// let mut wasm = WasmForeignObject::new("math.wasm").unwrap(); +/// wasm.init().unwrap(); +/// +/// // Or create with custom timeout (5 seconds) +/// let mut wasm = WasmForeignObject::with_timeout("math.wasm", 5.0).unwrap(); +/// +/// // Or create with custom memory limit (10 MB) +/// let mut wasm = WasmForeignObject::with_limits("math.wasm", 5.0, Some(10 * 1024 * 1024)).unwrap(); +/// +/// // Execute a function +/// let result = wasm.exec("add", &[5, 3]).unwrap(); +/// assert_eq!(result, vec![8]); +/// ``` +#[derive(Debug)] +pub struct WasmForeignObject { + /// WebAssembly binary + #[allow(dead_code)] + wasm_bytes: Vec, + /// Wasmtime engine + #[allow(dead_code)] + engine: Engine, + /// Wasmtime module + module: Module, + /// Wasmtime store (thread-safe) + store: RwLock>, + /// Wasmtime instance (thread-safe) + instance: RwLock>, + /// Cached function names (thread-safe) + func_names: Mutex>>, + /// Stop flag for epoch increment thread + stop_flag: Arc>, + /// Last function call results + last_results: Vec, + /// Timeout in seconds for WASM execution (default: 1.0 second) + timeout_seconds: f64, + /// Maximum memory size in bytes per linear memory (default: None = unlimited) + memory_size: Option, +} + +impl WasmForeignObject { + /// Calculate maximum ticks from timeout in seconds + /// + /// # Parameters + /// + /// * `timeout_seconds` - Timeout in seconds + /// + /// # Returns + /// + /// Number of ticks before timeout + fn calculate_max_ticks(timeout_seconds: f64) -> u64 { + // Ensure non-negative timeout (clamp to 0 for negative values) + // Casting is safe for reasonable timeout values (< 18 quadrillion seconds) + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let timeout_ms = (timeout_seconds.max(0.0) * 1000.0).round() as u64; + timeout_ms / WASM_EXECUTION_TICK_LENGTH_MS + } + + /// Create a new WebAssembly foreign object from a file with default timeout + /// + /// # Parameters + /// + /// * `path` - Path to the WebAssembly file (.wasm or .wat) + /// + /// # Returns + /// + /// A new WebAssembly foreign object with 1-second timeout + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or if WebAssembly compilation fails + pub fn new>(path: P) -> Result { + Self::with_timeout(path, DEFAULT_TIMEOUT_SECONDS) + } + + /// Create a new WebAssembly foreign object from a file with custom timeout + /// + /// # Parameters + /// + /// * `path` - Path to the WebAssembly file (.wasm or .wat) + /// * `timeout_seconds` - Timeout in seconds for WASM execution + /// + /// # Returns + /// + /// A new WebAssembly foreign object + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or if WebAssembly compilation fails + pub fn with_timeout>(path: P, timeout_seconds: f64) -> Result { + Self::with_limits(path, timeout_seconds, None) + } + + /// Create a new WebAssembly foreign object from a file with custom limits + /// + /// # Parameters + /// + /// * `path` - Path to the WebAssembly file (.wasm or .wat) + /// * `timeout_seconds` - Timeout in seconds for WASM execution + /// * `memory_size` - Optional maximum memory size in bytes per linear memory (None = unlimited) + /// + /// # Returns + /// + /// A new WebAssembly foreign object + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or if WebAssembly compilation fails + pub fn with_limits>( + path: P, + timeout_seconds: f64, + memory_size: Option, + ) -> Result { + // Read the WebAssembly file + let wasm_bytes = std::fs::read(path) + .map_err(|e| PecosError::Input(format!("Failed to read WebAssembly file: {e}")))?; + + Self::from_bytes_with_limits(&wasm_bytes, timeout_seconds, memory_size) + } + + /// Create a new WebAssembly foreign object from bytes with default timeout + /// + /// # Parameters + /// + /// * `wasm_bytes` - WebAssembly binary + /// + /// # Returns + /// + /// A new WebAssembly foreign object with 1-second timeout + /// + /// # Errors + /// + /// Returns an error if WebAssembly compilation fails + pub fn from_bytes(wasm_bytes: &[u8]) -> Result { + Self::from_bytes_with_limits(wasm_bytes, DEFAULT_TIMEOUT_SECONDS, None) + } + + /// Create a new WebAssembly foreign object from bytes with custom timeout + /// + /// # Parameters + /// + /// * `wasm_bytes` - WebAssembly binary + /// * `timeout_seconds` - Timeout in seconds for WASM execution + /// + /// # Returns + /// + /// A new WebAssembly foreign object + /// + /// # Errors + /// + /// Returns an error if WebAssembly compilation fails + pub fn from_bytes_with_timeout( + wasm_bytes: &[u8], + timeout_seconds: f64, + ) -> Result { + Self::from_bytes_with_limits(wasm_bytes, timeout_seconds, None) + } + + /// Create a new WebAssembly foreign object from bytes with custom limits + /// + /// # Parameters + /// + /// * `wasm_bytes` - WebAssembly binary + /// * `timeout_seconds` - Timeout in seconds for WASM execution + /// * `memory_size` - Optional maximum memory size in bytes per linear memory (None = unlimited) + /// + /// # Returns + /// + /// A new WebAssembly foreign object + /// + /// # Errors + /// + /// Returns an error if WebAssembly compilation fails + pub fn from_bytes_with_limits( + wasm_bytes: &[u8], + timeout_seconds: f64, + memory_size: Option, + ) -> Result { + // Create a new WebAssembly engine with epoch interruption enabled + let mut config = Config::new(); + config.epoch_interruption(true); + let engine = Engine::new(&config).map_err(|e| { + PecosError::Processing(format!("Failed to create WebAssembly engine: {e}")) + })?; + + // Create a new store with resource limits + let store_context = StoreContext::new(memory_size); + let mut store = Store::new(&engine, store_context); + + // Set the resource limiter + store.limiter(|ctx| &mut ctx.limits); + + // Compile the WebAssembly module + let module = Module::new(&engine, wasm_bytes).map_err(|e| { + PecosError::Processing(format!("Failed to compile WebAssembly module: {e}")) + })?; + + let stop_flag = Arc::new(RwLock::new(false)); + let engine_clone = engine.clone(); + let stop_flag_clone = stop_flag.clone(); + + // Start the epoch increment thread for timeout handling + thread::spawn(move || { + while !*stop_flag_clone.read() { + // Increment the epoch every tick length + engine_clone.increment_epoch(); + thread::sleep(Duration::from_millis(WASM_EXECUTION_TICK_LENGTH_MS)); + } + }); + + let mut foreign_object = Self { + wasm_bytes: wasm_bytes.to_vec(), + engine, + module, + store: RwLock::new(store), + instance: RwLock::new(None), + func_names: Mutex::new(None), + stop_flag, + last_results: Vec::new(), + timeout_seconds, + memory_size, + }; + + // Create the instance + foreign_object.new_instance()?; + + Ok(foreign_object) + } + + /// Get a function from the WebAssembly instance + /// + /// # Parameters + /// + /// * `func_name` - Name of the function to get + /// + /// # Returns + /// + /// The WebAssembly function + /// + /// # Errors + /// + /// Returns an error if the function is not found + fn get_function(&self, func_name: &str) -> Result { + // Get the instance + let instance = self.instance.read(); + let instance = instance + .as_ref() + .ok_or_else(|| PecosError::Resource("WebAssembly instance not created".to_string()))?; + + // Get the function + let mut store = self.store.write(); + let func = instance.get_func(&mut *store, func_name).ok_or_else(|| { + PecosError::Resource(format!("WebAssembly function '{func_name}' not found")) + })?; + + Ok(func) + } + + /// Call before each shot to reset variables (if function exists) + /// + /// This is a convenience method that calls the `shot_reinit` function in the + /// WebAssembly module if it exists. If the function doesn't exist, this is a no-op. + /// + /// # Errors + /// + /// Returns an error if the `shot_reinit` function exists but execution fails + pub fn shot_reinit(&mut self) -> Result<(), PecosError> { + let funcs = self.get_funcs(); + if funcs.contains(&"shot_reinit".to_string()) { + self.exec("shot_reinit", &[])?; + } + Ok(()) + } + + /// Get the WebAssembly binary bytes + /// + /// This is useful for serialization and cloning. + /// + /// # Returns + /// + /// A reference to the WebAssembly binary bytes + #[must_use] + pub fn wasm_bytes(&self) -> &[u8] { + &self.wasm_bytes + } + + /// Get the configured timeout in seconds + /// + /// # Returns + /// + /// The timeout in seconds for WASM execution + #[must_use] + pub fn timeout_seconds(&self) -> f64 { + self.timeout_seconds + } + + /// Get the configured memory size limit + /// + /// # Returns + /// + /// The memory size limit in bytes per linear memory (None = unlimited) + #[must_use] + pub fn memory_size(&self) -> Option { + self.memory_size + } +} + +impl ForeignObject for WasmForeignObject { + fn clone_box(&self) -> Box { + // Create a new instance from the same bytes with the same timeout and memory limit + let mut result = + Self::from_bytes_with_limits(&self.wasm_bytes, self.timeout_seconds, self.memory_size) + .expect("Failed to clone WasmForeignObject"); + + // Initialize it the same way + if self.instance.read().is_some() { + let _ = result.new_instance(); + } + + Box::new(result) + } + + fn init(&mut self) -> Result<(), PecosError> { + // Create a new instance + self.new_instance()?; + + // Check if the init function exists + let funcs = self.get_funcs(); + if !funcs.contains(&"init".to_string()) { + return Err(PecosError::Input( + "WebAssembly module must contain an 'init' function".to_string(), + )); + } + + // Call the init function + self.exec("init", &[])?; + + Ok(()) + } + + fn new_instance(&mut self) -> Result<(), PecosError> { + let mut store = self.store.write(); + + // Create a new instance + let instance = Instance::new(&mut *store, &self.module, &[]).map_err(|e| { + PecosError::Processing(format!("Failed to create WebAssembly instance: {e}")) + })?; + + // Store the instance + *self.instance.write() = Some(instance); + + Ok(()) + } + + fn get_funcs(&self) -> Vec { + // Check if we've already cached the function names + if let Some(ref funcs) = *self.func_names.lock() { + return funcs.clone(); + } + + // Get the function names + let mut funcs = Vec::new(); + for export in self.module.exports() { + if export.ty().func().is_some() { + funcs.push(export.name().to_string()); + } + } + + // Cache the function names + *self.func_names.lock() = Some(funcs.clone()); + + funcs + } + + fn exec(&mut self, func_name: &str, args: &[i64]) -> Result, PecosError> { + debug!("Executing WebAssembly function '{func_name}' with args {args:?}"); + + // Get the function + let func = self.get_function(func_name)?; + + // Get store early to check function signature + let mut store = self.store.write(); + let func_type = func.ty(&*store); + let param_types: Vec<_> = func_type.params().collect(); + + // Convert the arguments based on function signature + let wasm_args: Vec<_> = args + .iter() + .enumerate() + .map(|(i, a)| { + // Get the expected parameter type (or default to i32) + let param_type = param_types.get(i); + + match param_type { + Some(wasmtime::ValType::I64) => { + // Function expects i64, pass directly + wasmtime::Val::I64(*a) + } + Some(wasmtime::ValType::I32) | None => { + // Function expects i32 or unknown, convert with bounds checking + let value = if *a > i64::from(i32::MAX) { + warn!("Argument value {a} exceeds i32::MAX, clamping to i32::MAX"); + i32::MAX + } else if *a < i64::from(i32::MIN) { + warn!("Argument value {a} is less than i32::MIN, clamping to i32::MIN"); + i32::MIN + } else { + // Safe: we've verified the value is in range + i32::try_from(*a).expect("Value should be in range after bounds check") + }; + wasmtime::Val::I32(value) + } + _ => { + // Unsupported parameter type, default to i32 + warn!("Unexpected parameter type for argument {i}, defaulting to i32"); + let value = i32::try_from(*a).unwrap_or(i32::MAX); + wasmtime::Val::I32(value) + } + } + }) + .collect(); + + // Set execution deadline based on configured timeout + let max_ticks = Self::calculate_max_ticks(self.timeout_seconds); + store.set_epoch_deadline(max_ticks); + + // Get the number of results + let results_len = func_type.results().len(); + + // Handle functions based on their return type + let result = if results_len == 0 { + // Function returns nothing (like init) + func.call(&mut *store, &wasm_args, &mut []) + } else { + // Function returns something, create an appropriate buffer + let mut results_buffer = vec![Val::I32(0); results_len]; + debug!( + "Calling WebAssembly function '{func_name}' with args {wasm_args:?}, expecting {results_len} results" + ); + let res = func.call(&mut *store, &wasm_args, &mut results_buffer); + + // Store the results if successful + if res.is_ok() { + debug!("WebAssembly function returned {results_buffer:?}"); + self.last_results = results_buffer; + } + + res + }; + + // Handle the result + match result { + Ok(()) => { + if results_len == 0 { + // Functions with no return value + Ok(vec![0]) + } else { + // Convert the results back to i64 + let results: Vec = self + .last_results + .iter() + .map(|r| match r { + Val::I32(val) => i64::from(*val), + Val::I64(val) => *val, + _ => { + warn!("Unexpected result type from WebAssembly function"); + 0 + } + }) + .collect(); + + if results.is_empty() { + // If there are no results, return a zero + Ok(vec![0]) + } else { + Ok(results) + } + } + } + Err(e) => { + // Check if the error is a timeout + if let Some(trap) = e.downcast_ref::() + && trap.to_string().contains("interrupt") + { + return Err(PecosError::Processing(format!( + "WebAssembly function '{func_name}' timed out after {}s", + self.timeout_seconds + ))); + } + + Err(PecosError::Processing(format!( + "WebAssembly function '{func_name}' failed with error: {e}" + ))) + } + } + } + + fn teardown(&mut self) { + // Set the stop flag to stop the epoch increment thread + *self.stop_flag.write() = true; + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Drop for WasmForeignObject { + fn drop(&mut self) { + // Set the stop flag to stop the epoch increment thread + *self.stop_flag.write() = true; + } +} diff --git a/crates/pecos/Cargo.toml b/crates/pecos/Cargo.toml index 57488abb2..597438c3a 100644 --- a/crates/pecos/Cargo.toml +++ b/crates/pecos/Cargo.toml @@ -37,13 +37,19 @@ pecos-cppsparsesim = { workspace = true, optional = true } pecos-quest = { workspace = true, optional = true } pecos-qulacs = { workspace = true, optional = true } +# WebAssembly foreign object support (optional) +pecos-wasm = { workspace = true, optional = true } + [features] -default = ["selene", "qasm", "phir", "all-simulators"] +default = ["selene", "qasm", "phir", "wasm", "all-simulators"] qasm = [] llvm = ["pecos-qis-core/llvm", "pecos-hugr-qis", "pecos-hugr-qis?/llvm"] phir = [] selene = ["pecos-qis-selene"] +# WebAssembly support +wasm = ["pecos-wasm", "pecos-wasm/wasm"] + # Quantum simulator backends cppsparsesim = ["pecos-cppsparsesim"] quest = ["pecos-quest"] diff --git a/crates/pecos/src/lib.rs b/crates/pecos/src/lib.rs index 0b2d0a8c0..960cb291f 100644 --- a/crates/pecos/src/lib.rs +++ b/crates/pecos/src/lib.rs @@ -262,6 +262,28 @@ pub mod results { }; } +/// WebAssembly foreign object support +/// +/// This module provides WebAssembly execution support for classical computations +/// within PECOS quantum programs (QASM and PHIR). +/// +/// # Example +/// +/// ```rust,no_run +/// # #[cfg(feature = "wasm")] +/// # { +/// use pecos::wasm::WasmForeignObject; +/// use std::path::Path; +/// +/// // Load a WASM module +/// let wasm_obj = WasmForeignObject::new(Path::new("module.wasm")).unwrap(); +/// # } +/// ``` +#[cfg(feature = "wasm")] +pub mod wasm { + pub use pecos_wasm::{ForeignObject, WasmForeignObject}; +} + // ============================================================================ // Top-level re-exports for convenience and backward compatibility // ============================================================================ @@ -314,3 +336,7 @@ pub use pecos_quest::{ #[cfg(feature = "qulacs")] pub use pecos_qulacs::QulacsStateVec; + +// WebAssembly foreign object support +#[cfg(feature = "wasm")] +pub use pecos_wasm::{ForeignObject, WasmForeignObject}; diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index 59896ea4d..ea94ffb14 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -122,3 +122,10 @@ pub use pecos_quest::{QuestDensityMatrix, QuestStateVec}; #[cfg(feature = "qulacs")] pub use pecos_qulacs::QulacsStateVec; + +// ============================================================================ +// WebAssembly foreign object support +// ============================================================================ + +#[cfg(feature = "wasm")] +pub use pecos_wasm::{ForeignObject, WasmForeignObject}; diff --git a/python/pecos-rslib/rust/Cargo.toml b/python/pecos-rslib/rust/Cargo.toml index b0d88b919..095cdb577 100644 --- a/python/pecos-rslib/rust/Cargo.toml +++ b/python/pecos-rslib/rust/Cargo.toml @@ -19,10 +19,14 @@ doctest = false # Skip unit tests as well - all testing should be done through Python test = false +[features] +default = ["wasm"] +wasm = ["pecos/wasm"] + [dependencies] # Use the pecos metacrate which includes all simulators and runtimes by default -# Enable llvm feature explicitly for full Python functionality -pecos = { workspace = true, features = ["llvm"] } +# Enable llvm and wasm features explicitly for full Python functionality +pecos = { workspace = true, features = ["llvm", "wasm"] } pyo3 = { workspace=true, features = ["extension-module", "abi3-py310", "generate-import-lib"] } parking_lot.workspace = true diff --git a/python/pecos-rslib/rust/src/lib.rs b/python/pecos-rslib/rust/src/lib.rs index 1b6a4962d..caa92d596 100644 --- a/python/pecos-rslib/rust/src/lib.rs +++ b/python/pecos-rslib/rust/src/lib.rs @@ -36,6 +36,8 @@ mod sparse_stab_bindings; mod sparse_stab_engine_bindings; mod state_vec_bindings; mod state_vec_engine_bindings; +#[cfg(feature = "wasm")] +mod wasm_foreign_object_bindings; // Note: hugr_bindings module is currently disabled - conflicts with pecos-qis-interface due to duplicate symbols @@ -52,6 +54,8 @@ use sparse_stab_bindings::SparseSim; use sparse_stab_engine_bindings::PySparseStabEngine; use state_vec_bindings::RsStateVec; use state_vec_engine_bindings::PyStateVecEngine; +#[cfg(feature = "wasm")] +use wasm_foreign_object_bindings::PyWasmForeignObject; /// Clear the global JIT compilation cache (deprecated - JIT is no longer available) #[pyfunction] @@ -162,5 +166,9 @@ fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Utility functions m.add_function(wrap_pyfunction!(clear_jit_cache, m)?)?; + // WebAssembly foreign object (optional) + #[cfg(feature = "wasm")] + m.add_class::()?; + Ok(()) } diff --git a/python/pecos-rslib/rust/src/wasm_foreign_object_bindings.rs b/python/pecos-rslib/rust/src/wasm_foreign_object_bindings.rs new file mode 100644 index 000000000..5846d0d80 --- /dev/null +++ b/python/pecos-rslib/rust/src/wasm_foreign_object_bindings.rs @@ -0,0 +1,274 @@ +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +//! `PyO3` bindings for WebAssembly foreign object +//! +//! This module provides Python bindings for the Rust `WasmForeignObject` implementation, +//! allowing Python code to use the Rust Wasmtime runtime instead of the Python wasmtime package. + +use pecos::wasm::{ForeignObject, WasmForeignObject}; +use pyo3::exceptions::{PyException, PyFileNotFoundError, PyRuntimeError}; +use pyo3::prelude::*; +use pyo3::types::PyBytes; +use std::path::Path; + +/// Python wrapper for `WasmForeignObject` +/// +/// This class provides the same interface as the Python `WasmtimeObj` class, +/// but uses the Rust implementation under the hood for better performance +/// and thread safety. +#[pyclass(name = "RsWasmForeignObject")] +pub struct PyWasmForeignObject { + inner: WasmForeignObject, +} + +#[pymethods] +impl PyWasmForeignObject { + /// Create a new WebAssembly foreign object + /// + /// Args: + /// file: Path to WASM file (str or pathlib.Path) or WASM bytes (bytes) + /// timeout: Optional timeout in seconds (default: 1.0 second) + /// `memory_size`: Optional maximum memory size in bytes per linear memory (default: None = unlimited) + /// + /// Returns: + /// New WebAssembly foreign object instance + /// + /// Raises: + /// `FileNotFoundError`: If file path doesn't exist + /// `RuntimeError`: If WASM compilation fails + #[new] + #[pyo3(signature = (file, timeout=None, memory_size=None))] + fn new( + _py: Python<'_>, + file: &Bound<'_, PyAny>, + timeout: Option, + memory_size: Option, + ) -> PyResult { + let timeout_seconds = timeout.unwrap_or(1.0); + + // Try to extract as bytes first + if let Ok(bytes) = file.cast::() { + let wasm_bytes = bytes.as_bytes(); + let inner = + WasmForeignObject::from_bytes_with_limits(wasm_bytes, timeout_seconds, memory_size) + .map_err(|e| { + PyRuntimeError::new_err(format!("Failed to load WASM from bytes: {e}")) + })?; + return Ok(Self { inner }); + } + + // Try to extract as string path + if let Ok(path_str) = file.extract::() { + let path = Path::new(&path_str); + if !path.exists() { + return Err(PyFileNotFoundError::new_err(format!( + "WASM file not found: {path_str}" + ))); + } + + let inner = WasmForeignObject::with_limits(path, timeout_seconds, memory_size) + .map_err(|e| { + PyRuntimeError::new_err(format!("Failed to load WASM from file: {e}")) + })?; + return Ok(Self { inner }); + } + + // Try to handle pathlib.Path objects via __fspath__ protocol + if file.hasattr("__fspath__")? { + let path_str = file.call_method0("__fspath__")?.extract::()?; + let path = Path::new(&path_str); + if !path.exists() { + return Err(PyFileNotFoundError::new_err(format!( + "WASM file not found: {path_str}" + ))); + } + + let inner = WasmForeignObject::with_limits(path, timeout_seconds, memory_size) + .map_err(|e| { + PyRuntimeError::new_err(format!("Failed to load WASM from file: {e}")) + })?; + return Ok(Self { inner }); + } + + // If none of the above worked, return error + Err(PyException::new_err( + "Expected str (file path), pathlib.Path, or bytes (WASM binary)", + )) + } + + /// Initialize the WASM module + /// + /// This must be called before using the object. It creates a new instance + /// and calls the 'init' function in the WASM module. + /// + /// Raises: + /// `RuntimeError`: If init function is missing or execution fails + fn init(&mut self) -> PyResult<()> { + self.inner + .init() + .map_err(|e| PyRuntimeError::new_err(format!("Failed to initialize WASM: {e}"))) + } + + /// Reset variables before each shot + /// + /// Calls the '`shot_reinit`' function in the WASM module if it exists. + /// This is a no-op if the function doesn't exist. + /// + /// Raises: + /// `RuntimeError`: If `shot_reinit` function exists but execution fails + fn shot_reinit(&mut self) -> PyResult<()> { + self.inner + .shot_reinit() + .map_err(|e| PyRuntimeError::new_err(format!("Failed to call shot_reinit: {e}"))) + } + + /// Create a new WASM instance + /// + /// Resets the object's internal state by creating a fresh instance. + /// + /// Raises: + /// `RuntimeError`: If instance creation fails + fn new_instance(&mut self) -> PyResult<()> { + self.inner + .new_instance() + .map_err(|e| PyRuntimeError::new_err(format!("Failed to create new instance: {e}"))) + } + + /// Get list of exported function names + /// + /// Returns: + /// List of function names exported by the WASM module + fn get_funcs(&self) -> Vec { + self.inner.get_funcs() + } + + /// Execute a WASM function + /// + /// Args: + /// `func_name`: Name of the function to execute + /// args: List of integer arguments (i64) + /// + /// Returns: + /// Tuple containing the function results (or single 0 for void functions) + /// + /// Raises: + /// `RuntimeError`: If function not found or execution fails + #[allow(clippy::needless_pass_by_value)] // PyO3 extracts Python sequences as Vec + fn exec(&mut self, py: Python<'_>, func_name: &str, args: Vec) -> PyResult> { + let results = self.inner.exec(func_name, &args).map_err(|e| { + PyRuntimeError::new_err(format!("Failed to execute '{func_name}': {e}")) + })?; + + // Convert Vec to Python - single value as int, multiple as tuple + if results.len() == 1 { + // Return single value directly (matching Python behavior) + Ok(results[0].into_pyobject(py)?.into_any().unbind()) + } else { + // Return tuple for multiple values + let tuple = pyo3::types::PyTuple::new(py, results.iter())?; + Ok(tuple.into_any().unbind()) + } + } + + /// Cleanup resources + /// + /// Stops the epoch increment thread. This is called automatically + /// when the object is dropped, but can be called explicitly. + fn teardown(&mut self) { + self.inner.teardown(); + } + + /// Serialize to dictionary for pickling + /// + /// Returns: + /// Dictionary containing '`fobj_class`', '`wasm_bytes`', 'timeout', and '`memory_size`' + fn to_dict(&self, py: Python<'_>) -> PyResult> { + let dict = pyo3::types::PyDict::new(py); + + // Get the Python class for fobj_class + let module = py.import("pecos_rslib")?; + let cls = module.getattr("RsWasmForeignObject")?; + dict.set_item("fobj_class", cls)?; + + // Get WASM bytes + let wasm_bytes = PyBytes::new(py, self.inner.wasm_bytes()); + dict.set_item("wasm_bytes", wasm_bytes)?; + + // Get timeout + dict.set_item("timeout", self.inner.timeout_seconds())?; + + // Get memory_size (None or usize) + if let Some(size) = self.inner.memory_size() { + dict.set_item("memory_size", size)?; + } else { + dict.set_item("memory_size", py.None())?; + } + + Ok(dict.into()) + } + + /// Deserialize from dictionary (for pickling) + /// + /// Args: + /// `wasmtime_dict`: Dictionary containing '`fobj_class`', '`wasm_bytes`', and optionally 'timeout' and '`memory_size`' + /// + /// Returns: + /// New instance created from the dictionary + #[staticmethod] + fn from_dict(py: Python<'_>, wasmtime_dict: &Bound<'_, PyAny>) -> PyResult { + use pyo3::types::PyDictMethods; + let dict = wasmtime_dict.cast::()?; + let wasm_bytes = dict + .get_item("wasm_bytes")? + .ok_or_else(|| PyException::new_err("Missing 'wasm_bytes' in dictionary"))?; + + // Get timeout if present (default to 1.0 for backward compatibility) + let timeout = dict + .get_item("timeout")? + .and_then(|t| t.extract::().ok()); + + // Get memory_size if present (default to None for backward compatibility) + let memory_size = dict + .get_item("memory_size")? + .and_then(|m| m.extract::().ok()); + + Self::new(py, &wasm_bytes, timeout, memory_size) + } + + /// Support for pickle (Python serialization) + fn __getstate__(&self, py: Python<'_>) -> PyResult> { + self.to_dict(py) + } + + /// Support for pickle (Python deserialization) + fn __setstate__(&mut self, py: Python<'_>, state: &Bound<'_, PyAny>) -> PyResult<()> { + // Create new object and swap the inner value + let new_obj = Self::from_dict(py, state)?; + // Replace inner by creating a new instance from the same bytes with the same timeout and memory limit + let wasm_bytes = new_obj.inner.wasm_bytes(); + let timeout = new_obj.inner.timeout_seconds(); + let memory_size = new_obj.inner.memory_size(); + self.inner = WasmForeignObject::from_bytes_with_limits(wasm_bytes, timeout, memory_size) + .map_err(|e| { + PyRuntimeError::new_err(format!("Failed to deserialize WASM object: {e}")) + })?; + Ok(()) + } +} + +impl Drop for PyWasmForeignObject { + fn drop(&mut self) { + // Ensure teardown is called when the object is dropped + self.inner.teardown(); + } +} diff --git a/python/pecos-rslib/src/pecos_rslib/__init__.py b/python/pecos-rslib/src/pecos_rslib/__init__.py index f09792ad5..1ecce2399 100644 --- a/python/pecos-rslib/src/pecos_rslib/__init__.py +++ b/python/pecos-rslib/src/pecos_rslib/__init__.py @@ -27,6 +27,7 @@ ByteMessageBuilder, QuestDensityMatrix, QuestStateVec, + RsWasmForeignObject, ShotMap, ShotVec, SparseStabEngineRs, @@ -435,6 +436,8 @@ def get_compilation_backends() -> dict[str, Any]: # QuEST simulators "QuestStateVec", "QuestDensityMatrix", + # WebAssembly foreign object + "RsWasmForeignObject", # QIS engine (replaces Selene engine) "qis_engine", # QASM simulation - DEPRECATED: Use sim() instead diff --git a/python/quantum-pecos/docs/conf.py b/python/quantum-pecos/docs/conf.py index 9c8b2b284..ec399ac27 100644 --- a/python/quantum-pecos/docs/conf.py +++ b/python/quantum-pecos/docs/conf.py @@ -250,7 +250,7 @@ # -- Options for autodoc extension ---------------------------------------------- # Mock modules that are not available during doc build -autodoc_mock_imports = ["wasmer", "wasmtime", "cupy", "pecos.slr.std", "pecos.slr.slr"] +autodoc_mock_imports = ["cupy", "pecos.slr.std", "pecos.slr.slr"] # Skip problematic modules autosummary_mock_imports = [ diff --git a/python/quantum-pecos/pyproject.toml b/python/quantum-pecos/pyproject.toml index 7a34e989b..e828f503d 100644 --- a/python/quantum-pecos/pyproject.toml +++ b/python/quantum-pecos/pyproject.toml @@ -62,31 +62,14 @@ qir = [ guppy = [ "guppylang>=0.21.0", # Install guppylang first "selene-sim~=0.2.0", # Then selene-sim (dependency of guppylang) - # hugr package is not directly needed - comes via guppylang/selene-sim -] -wasmtime = [ - "wasmtime>=13.0" ] visualization = [ "plotly~=5.9.0", ] -wasm-all = [ - "quantum-pecos[wasmtime]", - "quantum-pecos[wasmer]; python_version < '3.13'", -] all = [ - "quantum-pecos[qir]", - "quantum-pecos[simulators]", - "quantum-pecos[wasm-all]", "quantum-pecos[visualization]", "quantum-pecos[guppy]", "quantum-pecos[qir]", - "quantum-pecos[guppy]", -] -# The following only work for some environments/Python versions: -wasmer = [ - "wasmer~=1.1.0", # Package not currently compatible with Python 3.13 - "wasmer_compiler_cranelift~=1.1.0", ] # CUDA dependencies. See docs/user-guide/cuda-setup.md for detailed installation instructions diff --git a/python/quantum-pecos/src/pecos/engines/cvm/wasm.py b/python/quantum-pecos/src/pecos/engines/cvm/wasm.py index bc2bfe895..dfc49e287 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/wasm.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/wasm.py @@ -23,7 +23,6 @@ from pecos.engines.cvm.binarray import BinArray from pecos.engines.cvm.sim_func import sim_exec -from pecos.engines.cvm.wasm_vms.wasmer import read_wasmer from pecos.engines.cvm.wasm_vms.wasmtime import read_wasmtime from pecos.errors import MissingCCOPError @@ -97,12 +96,6 @@ def get_ccop(circuit: QuantumCircuit) -> CCOPObject | None: elif ccop_type == "wasmtime": ccop = read_wasmtime(ccop) - elif ccop_type in {"wasmer", "wasmer_cl"}: - ccop = read_wasmer(ccop, compiler="wasmer_cl") - - elif ccop_type == "wasmer_llvm": - ccop = read_wasmer(ccop, compiler=ccop_type) - elif ccop_type in {"obj", "object"}: pass diff --git a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmer.py b/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmer.py deleted file mode 100644 index 7eefdf61a..000000000 --- a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmer.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2022 The PECOS Developers -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License.You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -"""Wasmer WebAssembly runtime integration. - -This module provides integration with the Wasmer WebAssembly runtime for -executing compiled classical functions in the PECOS framework. -""" - -from __future__ import annotations - -import contextlib -import sys -from pathlib import Path -from typing import TYPE_CHECKING - -from pecos.engines.cvm.sim_func import sim_funcs - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any - -with contextlib.suppress(ImportError): - from wasmer import Instance, Module, Store, engine - -with contextlib.suppress(ImportError): - from wasmer_compiler_cranelift import Compiler as CompilerCranelift - -with contextlib.suppress(ImportError): - from wasmer_compiler_llvm import Compiler as CompilerLLVM - - -class WasmerInstance: - """Wrapper class to create a wasmer instance and access its functions.""" - - def __init__(self, file: str | bytes, compiler: str = "wasm_cl") -> None: - """Initialize a Wasmer WebAssembly instance. - - Args: - file: Path to a WebAssembly file or raw WebAssembly bytes. - compiler: The compiler backend to use. Options are 'wasm_cl' for - Cranelift (default) or 'wasm_llvm' for LLVM. - - Raises: - ImportError: If the wasmer module is not installed. - """ - if "wasmer" not in sys.modules: - msg = 'wasmer is being called but not installed! Install "wasmer"' - raise ImportError(msg) - if isinstance(file, str): - with Path.open(file, "rb") as f: - wasm_b = f.read() - else: - wasm_b = file - - store = ( - Store(engine.JIT(CompilerLLVM)) - if compiler == "wasm_llvm" - else Store(engine.JIT(CompilerCranelift)) - ) - - module = Module(store, wasm_b) - instance = Instance(module) - - self.wasm = instance - self.module = module - - def get_funcs(self) -> list[str]: - """Get list of available function names from the WASM module. - - Returns: - List of function names that can be executed. - """ - return [ - str(f.name) - for f in self.module.exports - if str(f.type).startswith("FunctionType") - ] - - def exec( - self, - func_name: str, - args: Sequence[tuple[Any, int]], - *, - debug: bool = False, - ) -> int: - """Execute a WASM function with given arguments. - - Args: - func_name: Name of the function to execute. - args: Sequence of (type, value) tuples for arguments. - debug: Whether to use debug simulation functions. - - Returns: - Integer result from the function execution. - """ - if debug and func_name.startswith("sim_"): - method = sim_funcs[func_name] - return method(*args) - - method = getattr(self.wasm.exports, func_name) - args = [int(b) for _, b in args] - return method(*args) - - def teardown(self) -> None: - """Clean up resources (no-op for Wasmer).""" - # Only needed for wasmtime - - -def read_wasmer(path: str | bytes, compiler: str = "wasm_cl") -> WasmerInstance: - """Helper method to create a wasmer instance.""" - return WasmerInstance(path, compiler) diff --git a/python/quantum-pecos/src/pecos/foreign_objects/wasmer.py b/python/quantum-pecos/src/pecos/foreign_objects/wasmer.py deleted file mode 100644 index 5d53e3915..000000000 --- a/python/quantum-pecos/src/pecos/foreign_objects/wasmer.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Wasmer WebAssembly runtime integration for PECOS. - -This module provides integration with the Wasmer WebAssembly runtime, enabling the execution of WASM modules for -classical computations within the PECOS quantum error correction framework. It supports compilation, instantiation, -and execution of WebAssembly code with proper error handling and resource management. -""" - -# Copyright 2022 The PECOS Developers -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License.You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -from wasmer import FunctionType, Instance, Module, Store, engine -from wasmer_compiler_cranelift import Compiler as Cranelift - -from pecos.errors import MissingCCOPError, WasmRuntimeError - -if TYPE_CHECKING: - from collections.abc import Sequence - - -class WasmerObj: - """Wrapper class to create a Wasmer instance and access its functions. - - For more info on using Wasmer, see: https://wasmerio.github.io/wasmer-python/api/wasmer/wasmer.html - """ - - def __init__( - self, - file: str | bytes | Path, - compiler: object | None = None, - ) -> None: - """Initialize a WasmerObj. - - Args: - ---- - file: Path to WASM file, file bytes, or Path object to load. - compiler: Optional Wasmer compiler to use. Defaults to Cranelift if None. - """ - self.compiler = compiler - - if isinstance(file, str | Path): - with Path.open(Path(file), "rb") as f: - wasm_bytes = f.read() - else: - wasm_bytes = file - - self.wasm_bytes = wasm_bytes - - self.module = None - self.instance = None - self.func_names = None - - self.spin_up_wasm() - - def init(self) -> None: - """Initialize object before running a series of experiments.""" - self.new_instance() - self.get_funcs() - - if "init" not in self.get_funcs(): - msg = "Missing `init()` from Wasm module." - raise Exception(msg) - - self.exec("init", []) - - def shot_reinit(self) -> None: - """Call before each shot to, e.g., reset variables.""" - if "shot_reinit" in self.get_funcs(): - self.exec("shot_reinit", []) - - def new_instance(self) -> None: - """Reset object internal state.""" - self.instance = Instance(self.module) - - def spin_up_wasm(self) -> None: - """Initialize the WASM module and create a new instance.""" - compiler = self.compiler - if compiler is None: - compiler = Cranelift - - store = Store(engine.JIT(compiler)) - - self.module = Module(store, self.wasm_bytes) - self.new_instance() - - def get_funcs(self) -> list[str]: - """Get list of function names exported by the WASM module. - - Returns: - List of function names available for execution. - """ - if self.func_names is None: - fs = [ - str(f.name) - for f in self.module.exports - if isinstance(f.type, FunctionType) - ] - - self.func_names = fs - - return self.func_names - - def exec(self, func_name: str, args: Sequence) -> tuple: - """Execute a function in the WASM module. - - Args: - func_name: Name of the function to execute. - args: Sequence of arguments to pass to the function. - - Returns: - Tuple containing the function result. - - Raises: - WasmRuntimeError: If WASM execution fails. - """ - try: - func = getattr(self.instance.exports, func_name) - except AttributeError as e: - message = f"Func {func_name} not found in WASM" - raise MissingCCOPError(message) from e - - params = func.type.params - if len(args) != len(params): - msg = f"Wasmer function `{func_name}` takes {len(params)} args and {len(args)} were given!" - raise WasmRuntimeError(msg) - - try: - return func(*args) - except Exception as ex: - raise WasmRuntimeError(ex.args[0]) from ex - - def to_dict(self) -> dict: - """Convert the WasmerObj to a dictionary for serialization. - - Returns: - Dictionary containing the object class and WASM bytes. - """ - return {"fobj_class": WasmerObj, "wasm_bytes": self.wasm_bytes} - - @staticmethod - def from_dict(wasmer_dict: dict) -> WasmerObj: - """Create a WasmerObj from a dictionary. - - Args: - wasmer_dict: Dictionary containing object class and WASM bytes. - - Returns: - New WasmerObj instance. - """ - return wasmer_dict["fobj_class"](wasmer_dict["wasm_bytes"]) diff --git a/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py b/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py index 021348d75..647407db5 100644 --- a/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py +++ b/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py @@ -1,10 +1,3 @@ -"""Wasmtime WebAssembly runtime integration for PECOS. - -This module provides integration with the Wasmtime WebAssembly runtime, enabling high-performance execution of WASM -modules for classical computations within the PECOS quantum error correction framework. It supports advanced features -like timeout handling, resource limits, and secure sandboxed execution of WebAssembly code. -""" - # Copyright 2022 The PECOS Developers # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with @@ -16,87 +9,86 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. +"""Wasmtime WebAssembly runtime integration for PECOS. + +This module provides integration with the Wasmtime WebAssembly runtime, enabling high-performance execution of WASM +modules for classical computations within the PECOS quantum error correction framework. + +This is now a thin wrapper around the Rust implementation (RsWasmForeignObject) from pecos-rslib, +which provides better performance and thread safety compared to the previous Python implementation. +""" + from __future__ import annotations -from pathlib import Path -from threading import Event from typing import TYPE_CHECKING -from wasmtime import Config, Engine, FuncType, Instance, Module, Store, Trap, TrapCode - -from pecos.errors import MissingCCOPError, WasmRuntimeError -from pecos.foreign_objects.wasm_execution_timer_thread import ( - WASM_EXECUTION_MAX_TICKS, - WASM_EXECUTION_TICK_LENGTH_S, - WasmExecutionTimerThread, -) +from pecos_rslib._pecos_rslib import RsWasmForeignObject if TYPE_CHECKING: from collections.abc import Sequence + from pathlib import Path class WasmtimeObj: - """Wrapper class to create a wasmtime instance and access its functions. + """Wrapper class for Wasmtime WebAssembly runtime using Rust implementation. - For more info on using Wasmer, see: https://wasmerio.github.io/wasmer-python/api/wasmer/wasmer.html + This class provides a Python-friendly interface to the Rust-based WasmForeignObject, + maintaining API compatibility with the previous Python implementation. + + The Rust implementation provides: + - Better performance through native code execution + - Thread-safe operation with RwLock/Mutex synchronization + - Configurable timeout (default: 1 second to match old Python version) + - Configurable memory limits (default: unlimited) + - Support for both i32 and i64 parameter types """ - def __init__(self, file: str | bytes | Path) -> None: - """Initialize a WasmtimeObj. + def __init__( + self, + file: str | bytes | Path, + timeout: float | None = None, + memory_size: int | None = None, + ) -> None: + """Initialize a WasmtimeObj using the Rust implementation. Args: - ---- - file: Path to WASM file, file bytes, or Path object to load. + file: Path to WASM file (.wasm or .wat), file bytes, or Path object to load. + WAT files are automatically compiled to WASM by the Rust runtime. + timeout: Optional timeout in seconds for WASM execution (default: 1.0 second). + memory_size: Optional maximum memory size in bytes per linear memory (default: None = unlimited). + For example, 10 * 1024 * 1024 for 10 MB limit. """ - if isinstance(file, str | Path): - with Path.open(Path(file), "rb") as f: - wasm_bytes = f.read() - else: - wasm_bytes = file - - self.wasm_bytes = wasm_bytes - - self.store = None - self.module = None - self.instance = None - self.func_names = None + # Create the Rust object with optional timeout and memory limit + self._rust_obj = RsWasmForeignObject( + file, + timeout=timeout, + memory_size=memory_size, + ) - self.spin_up_wasm() + # Get WASM bytes for compatibility with serialization + self.wasm_bytes = self._rust_obj.to_dict()["wasm_bytes"] def init(self) -> None: - """Initialize object before running a series of experiments.""" - self.new_instance() - self.get_funcs() + """Initialize object before running a series of experiments. - if "init" not in self.get_funcs(): - msg = "Missing `init()` from Wasm module." - raise Exception(msg) + This creates a new WASM instance and calls the 'init' function. - self.exec("init", []) + Raises: + RuntimeError: If the 'init' function is not exported by the WASM module. + """ + self._rust_obj.init() def shot_reinit(self) -> None: - """Call before each shot to, e.g., reset variables.""" - if "shot_reinit" in self.get_funcs(): - self.exec("shot_reinit", []) + """Call before each shot to reset variables. + + This calls the 'shot_reinit' function in the WASM module if it exists. + It's a no-op if the function is not present. + """ + self._rust_obj.shot_reinit() def new_instance(self) -> None: - """Reset object internal state.""" - self.instance = Instance(self.store, self.module, []) - - def spin_up_wasm(self) -> None: - """Initialize the WASM module with epoch interruption and start timer thread.""" - config = Config() - config.epoch_interruption = True - engine = Engine(config) - self.store = Store(engine) - self.module = Module(self.store.engine, self.wasm_bytes) - self.stop_flag = Event() - self.inc_thread_handle = WasmExecutionTimerThread( - self.stop_flag, - self._increment_engine, - ) - self.inc_thread_handle.start() - self.new_instance() + """Reset object internal state by creating a new WASM instance.""" + self._rust_obj.new_instance() def get_funcs(self) -> list[str]: """Get list of function names exported by the WASM module. @@ -104,99 +96,75 @@ def get_funcs(self) -> list[str]: Returns: List of function names available for execution. """ - if self.func_names is None: - fs = [ - str(f.name) for f in self.module.exports if isinstance(f.type, FuncType) - ] - - self.func_names = fs - - return self.func_names - - def _increment_engine(self) -> None: - self.store.engine.increment_epoch() + return self._rust_obj.get_funcs() def exec(self, func_name: str, args: Sequence) -> tuple: """Execute a function in the WASM module with timeout protection. Args: func_name: Name of the function to execute. - args: Sequence of arguments to pass to the function. + args: Sequence of arguments to pass to the function (will be converted to i64). Returns: - Tuple containing the function result. + Tuple containing the function result(s). Single values are returned as (value,). Raises: - MissingCCOPError: If function not found in WASM module. - WasmRuntimeError: If WASM execution fails or times out. + RuntimeError: If function not found or execution fails/times out. + + Notes: + The Rust implementation automatically handles i32/i64 type conversion based on + the function signature, with bounds checking for i32 parameters. + + Default timeout is 1 second, but can be configured via the constructor. """ - try: - func = self.instance.exports(self.store)[func_name] - except KeyError as e: - message = f"No method found with name {func_name} in WASM" - raise MissingCCOPError(message) from e + # Convert args to list of i64 + args_list = [int(a) for a in args] - try: - self.store.engine.increment_epoch() - self.store.set_epoch_deadline(WASM_EXECUTION_MAX_TICKS) - output = func(self.store, *args) - except Trap as t: - if t.trap_code is TrapCode.INTERRUPT: - message = ( - f"WASM error: WASM failed during run-time. Execution time of " - f"function '{func_name}' exceeded maximum " - f"{WASM_EXECUTION_MAX_TICKS * WASM_EXECUTION_TICK_LENGTH_S}s" - ) - else: - message = ( - f"WASM error: WASM failed during run-time. Execution of " - f"function '{func_name}' resulted in {t.trap_code}\n" - f"{t.message}" - ) - raise WasmRuntimeError(message) from t - except Exception as e: - message = ( - f"Error during execution of function '{func_name}' with args {args}" - ) - raise WasmRuntimeError(message) from e - - return output + # Execute via Rust - it returns either a single value or tuple + result = self._rust_obj.exec(func_name, args_list) + + # Ensure we always return a tuple for API compatibility + if isinstance(result, (list, tuple)): + return tuple(result) + return (result,) def teardown(self) -> None: - """Cleanup resources by stopping the timer thread.""" - self.stop_flag.set() - self.inc_thread_handle.join() + """Cleanup resources by stopping the epoch increment thread.""" + self._rust_obj.teardown() def __del__(self) -> None: """Ensure cleanup happens when object is garbage collected.""" try: - if ( - hasattr(self, "stop_flag") - and hasattr(self, "inc_thread_handle") - and not self.stop_flag.is_set() - ): - self.teardown() - except Exception as e: # noqa: BLE001 - # Broad exception handling is required in __del__ to prevent errors during interpreter shutdown. - # Any exception type could be raised, and logging is not safe at this stage. - del e # Explicitly acknowledge we're ignoring the exception + if hasattr(self, "_rust_obj"): + self._rust_obj.teardown() + except Exception: # noqa: BLE001, S110 + # Broad exception handling is required in __del__ to prevent errors during + # interpreter shutdown. We silently ignore all exceptions. + pass def to_dict(self) -> dict: """Convert the WasmtimeObj to a dictionary for serialization. Returns: - Dictionary containing the object class and WASM bytes. + Dictionary containing the object class and WASM bytes for pickling. """ return {"fobj_class": WasmtimeObj, "wasm_bytes": self.wasm_bytes} @staticmethod def from_dict(wasmtime_dict: dict) -> WasmtimeObj: - """Create a WasmtimeObj from a dictionary. + """Create a WasmtimeObj from a dictionary (for unpickling). Args: - wasmtime_dict: Dictionary containing object class and WASM bytes. + wasmtime_dict: Dictionary containing object class, WASM bytes, and optionally timeout and memory_size. Returns: New WasmtimeObj instance. """ - return wasmtime_dict["fobj_class"](wasmtime_dict["wasm_bytes"]) + # Get timeout and memory_size if present (for backward compatibility, defaults are handled by __init__) + timeout = wasmtime_dict.get("timeout") + memory_size = wasmtime_dict.get("memory_size") + return wasmtime_dict["fobj_class"]( + wasmtime_dict["wasm_bytes"], + timeout=timeout, + memory_size=memory_size, + ) diff --git a/python/quantum-pecos/tests/pecos/integration/test_phir.py b/python/quantum-pecos/tests/pecos/integration/test_phir.py index 4c0656a25..91c1decd7 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_phir.py +++ b/python/quantum-pecos/tests/pecos/integration/test_phir.py @@ -23,14 +23,6 @@ from phir.model import PHIRModel from pydantic import ValidationError -try: - from pecos.foreign_objects.wasmer import WasmerObj - - WASMER_ERR_MSG = None -except ImportError as e: - WasmerObj = None - WASMER_ERR_MSG = str(e) - # tools for converting wasm to wat: https://github.com/WebAssembly/wabt/releases/tag/1.0.33 this_dir = Path(__file__).parent @@ -49,23 +41,6 @@ # run all without optional_dependency tests: pytest -v -m "not optional_dependency" -def is_wasmer_supported() -> bool: - """A check on whether Wasmer is known to support OS/Python versions. - - Note: wasmer-python currently only supports Python 3.7-3.10. - See: https://github.com/wasmerio/wasmer-python/issues/778 (Python 3.12) - https://github.com/wasmerio/wasmer-python/issues/696 (Python 3.11) - - Future considerations: - - Consider dropping wasmer-python in favor of Wasmtime (which is actively maintained) - - Alternative: Implement Wasmer support through Rust bindings for cross-platform/version compatibility - - These tests are currently redundant with Wasmtime tests - """ - return WASMER_ERR_MSG != "Wasmer is not available on this system" - - -@pytest.mark.wasmtime -@pytest.mark.optional_dependency def test_spec_example_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS.""" wasm = WasmtimeObj(math_wat) @@ -76,8 +51,6 @@ def test_spec_example_wasmtime() -> None: ) -@pytest.mark.wasmtime -@pytest.mark.optional_dependency def test_spec_example_noisy_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS, with noise.""" wasm = WasmtimeObj(str(math_wat)) @@ -103,8 +76,6 @@ def test_spec_example_noisy_wasmtime() -> None: ) -@pytest.mark.wasmtime -@pytest.mark.optional_dependency def test_example1_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS.""" wasm = WasmtimeObj(add_wat) @@ -115,8 +86,6 @@ def test_example1_wasmtime() -> None: ) -@pytest.mark.wasmtime -@pytest.mark.optional_dependency def test_example1_noisy_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS, with noise.""" wasm = WasmtimeObj(str(add_wat)) @@ -142,57 +111,6 @@ def test_example1_noisy_wasmtime() -> None: ) -@pytest.mark.skipif( - not is_wasmer_supported(), - reason="Wasmer is not supported on some OS/Python version combinations. " - "wasmer-python only supports Python 3.7-3.10 (current Python 3.11+). " - "Wasmtime tests provide equivalent coverage.", -) -@pytest.mark.wasmer -@pytest.mark.optional_dependency -def test_example1_wasmer() -> None: - """A random example showing that various basic aspects of PHIR is runnable by PECOS.""" - wasm = WasmerObj(add_wat) - HybridEngine().run( - program=example1_phir, - foreign_object=wasm, - shots=1000, - ) - - -@pytest.mark.skipif( - not is_wasmer_supported(), - reason="Wasmer is not supported on some OS/Python version combinations. " - "wasmer-python only supports Python 3.7-3.10 (current Python 3.11+). " - "Wasmtime tests provide equivalent coverage.", -) -@pytest.mark.wasmer -@pytest.mark.optional_dependency -def test_example1_noisy_wasmer() -> None: - """A random example showing that various basic aspects of PHIR is runnable by PECOS, with noise.""" - wasm = WasmerObj(str(add_wat)) - generic_errors = GenericErrorModel( - error_params={ - "p1": 2e-1, - "p2": 2e-1, - "p_meas": 2e-1, - "p_init": 1e-1, - "p1_error_model": { - "X": 0.25, - "Y": 0.25, - "Z": 0.25, - "L": 0.25, - }, - }, - ) - sim = HybridEngine(error_model=generic_errors) - sim.run( - program=example1_phir, - foreign_object=wasm, - shots=1000, - ) - - def test_example1_no_wasm() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS, without Wasm.""" HybridEngine().run(program=example1_no_wasm_phir, shots=1000) diff --git a/python/quantum-pecos/tests/pytest.ini b/python/quantum-pecos/tests/pytest.ini index 8949030cb..490ea97d2 100644 --- a/python/quantum-pecos/tests/pytest.ini +++ b/python/quantum-pecos/tests/pytest.ini @@ -12,8 +12,6 @@ markers = # slow: mark test as slow. optional_dependency: mark a test as using one or more optional dependencies. optional_unix: mark tests as using an optional dependency that only work with Unix-based systems. - wasmer: mark test as using the "wasmer" option. - wasmtime: mark test as using the "wasmtime" option. # Ignore deprecation warnings from external libraries that we cannot control filterwarnings = diff --git a/uv.lock b/uv.lock index 8a2c72a23..d90d45a70 100644 --- a/uv.lock +++ b/uv.lock @@ -3555,9 +3555,6 @@ all = [ { name = "llvmlite", marker = "python_full_version < '3.13'" }, { name = "plotly" }, { name = "selene-sim" }, - { name = "wasmer", marker = "python_full_version < '3.13'" }, - { name = "wasmer-compiler-cranelift", marker = "python_full_version < '3.13'" }, - { name = "wasmtime" }, ] cuda = [ { name = "cupy-cuda13x", marker = "python_full_version >= '3.11'" }, @@ -3574,18 +3571,6 @@ qir = [ visualization = [ { name = "plotly" }, ] -wasm-all = [ - { name = "wasmer", marker = "python_full_version < '3.13'" }, - { name = "wasmer-compiler-cranelift", marker = "python_full_version < '3.13'" }, - { name = "wasmtime" }, -] -wasmer = [ - { name = "wasmer" }, - { name = "wasmer-compiler-cranelift" }, -] -wasmtime = [ - { name = "wasmtime" }, -] [package.metadata] requires-dist = [ @@ -3602,18 +3587,11 @@ requires-dist = [ { name = "pytket-cutensornet", marker = "python_full_version >= '3.11' and extra == 'cuda'", specifier = ">=0.12.0" }, { name = "quantum-pecos", extras = ["guppy"], marker = "extra == 'all'" }, { name = "quantum-pecos", extras = ["qir"], marker = "extra == 'all'" }, - { name = "quantum-pecos", extras = ["simulators"], marker = "extra == 'all'" }, { name = "quantum-pecos", extras = ["visualization"], marker = "extra == 'all'" }, - { name = "quantum-pecos", extras = ["wasm-all"], marker = "extra == 'all'" }, - { name = "quantum-pecos", extras = ["wasmer"], marker = "python_full_version < '3.13' and extra == 'wasm-all'" }, - { name = "quantum-pecos", extras = ["wasmtime"], marker = "extra == 'wasm-all'" }, { name = "scipy", specifier = ">=1.1.0" }, { name = "selene-sim", marker = "extra == 'guppy'", specifier = "~=0.2.0" }, - { name = "wasmer", marker = "extra == 'wasmer'", specifier = "~=1.1.0" }, - { name = "wasmer-compiler-cranelift", marker = "extra == 'wasmer'", specifier = "~=1.1.0" }, - { name = "wasmtime", marker = "extra == 'wasmtime'", specifier = ">=13.0" }, ] -provides-extras = ["qir", "guppy", "wasmtime", "visualization", "wasm-all", "all", "wasmer", "cuda"] +provides-extras = ["qir", "guppy", "visualization", "all", "cuda"] [[package]] name = "qwasm" @@ -4347,28 +4325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] -[[package]] -name = "wasmer" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/0a/9e5efd92e5cf24d5c08030b4f76dcdf10cbc55c639bbf4df8aeb0c76d448/wasmer-1.1.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c2af4b907ae2dabcac41e316e811d5937c93adf1f8b05c5d49427f8ce0f37630", size = 1465236, upload-time = "2022-01-07T23:23:52.601Z" }, - { url = "https://files.pythonhosted.org/packages/a7/79/3f53cf611cbdd04a9b9997bf3ad18e5602350f90d404c162fbf3112684f2/wasmer-1.1.0-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:ab1ae980021e5ec0bf0c6cdd3b979b1d15a5f3eb2b8a32da8dcb1156e4a1e484", size = 1603286, upload-time = "2022-01-07T23:23:54.467Z" }, - { url = "https://files.pythonhosted.org/packages/24/70/ca7bf7a3f85d8de745eca73e40bc83cf86bb52ea494b33721fc0572889ab/wasmer-1.1.0-cp310-none-win_amd64.whl", hash = "sha256:d0d93aec6215893d33e803ef0a8d37bf948c585dd80ba0e23a83fafee820bc03", size = 1435852, upload-time = "2022-01-07T23:23:56.134Z" }, - { url = "https://files.pythonhosted.org/packages/39/6b/30e25924cae7add377f5601e71c778e9a1e515c7a58291f52756c1bb7e87/wasmer-1.1.0-py3-none-any.whl", hash = "sha256:2caf8c67feae9cd4246421551036917811c446da4f27ad4c989521ef42751931", size = 1617, upload-time = "2022-01-07T23:24:10.046Z" }, -] - -[[package]] -name = "wasmer-compiler-cranelift" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/cf/e0ac8ec0dde10716668819eb894edddd15cece14fcb4c26ee82ed3d1f65e/wasmer_compiler_cranelift-1.1.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9869910179f39696a020edc5689f7759257ac1cce569a7a0fcf340c59788baad", size = 1731266, upload-time = "2022-01-07T23:24:11.614Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/1cb2c909ef82f464afed1cd799cfcae95063fc3233c5140819e4b4ceee54/wasmer_compiler_cranelift-1.1.0-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:405546ee864ac158a4107f374dfbb1c8d6cfb189829bdcd13050143a4bd98f28", size = 1854396, upload-time = "2022-01-07T23:24:12.878Z" }, - { url = "https://files.pythonhosted.org/packages/a1/46/bf489a62c2f919ffcfce4b92c9d07e0257b14a1afd4fb122a7b64632c8ec/wasmer_compiler_cranelift-1.1.0-cp310-none-win_amd64.whl", hash = "sha256:bdf75af9ef082e6aeb752550f694273340ece970b65099e0746db0f972760d11", size = 1705776, upload-time = "2022-01-07T23:24:14.097Z" }, - { url = "https://files.pythonhosted.org/packages/28/fa/26489c8f25470a3d50994aac8ebeabb2ca7f88874a15e0e77272b3a912c4/wasmer_compiler_cranelift-1.1.0-py3-none-any.whl", hash = "sha256:200fea80609cfb088457327acf66d5aa61f4c4f66b5a71133ada960b534c7355", size = 1866, upload-time = "2022-01-07T23:24:26.736Z" }, -] - [[package]] name = "wasmtime" version = "38.0.0"