Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 92 additions & 90 deletions Cargo.lock

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ check-cli:
exit 1
fi

# Check if the installed CLI might be stale
stale=false
reasons=()

# Check 1: version mismatch
expected_version=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
installed_version=$(pecos --version 2>/dev/null | awk '{print $2}')
if [[ "$installed_version" != "$expected_version" ]]; then
stale=true
reasons+=("Version mismatch (installed: ${installed_version:-unknown}, expected: $expected_version)")
fi

# Check 2: missing expected subcommands
if ! pecos rust --help >/dev/null 2>&1; then
stale=true
reasons+=("Missing 'rust' subcommand")
fi

if [[ "$stale" == "true" ]]; then
echo ""
echo "Warning: PECOS CLI may be outdated."
for reason in "${reasons[@]}"; do
echo " - $reason"
done
echo ""
echo " Update with: just reinstall-cli"
echo ""
fi

# Informational: suggest CUDA Python packages if toolkit available but cupy isn't
if pecos cuda check -q >/dev/null 2>&1; then
if ! python -c "import cupy" >/dev/null 2>&1; then
Expand Down
21 changes: 12 additions & 9 deletions crates/pecos-wasm/src/wasmtime_foreign_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,18 +564,21 @@ impl ForeignObject for WasmForeignObject {
}
}
Err(e) => {
// Check if the error is a timeout
if let Some(trap) = e.downcast_ref::<Trap>()
&& trap.to_string().contains("interrupt")
{
return Err(PecosError::Processing(format!(
"WebAssembly function '{func_name}' timed out after {}s",
self.timeout_seconds
)));
if let Some(trap) = e.downcast_ref::<Trap>() {
let trap_str = trap.to_string();
if trap_str.contains("interrupt") {
return Err(PecosError::Processing(format!(
"WebAssembly execution timed out after {}s (possible infinite loop in '{func_name}')",
self.timeout_seconds
)));
}
if trap_str.contains("integer divide by zero") {
return Err(PecosError::RuntimeDivisionByZero);
}
}

Err(PecosError::Processing(format!(
"WebAssembly function '{func_name}' failed with error: {e}"
"WebAssembly function '{func_name}' trapped: {e}"
)))
}
}
Expand Down
2 changes: 2 additions & 0 deletions python/pecos-rslib/pecos_rslib.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,8 @@ def get_compilation_backends() -> dict[str, object]:
# =============================================================================
# WASM
# =============================================================================
class WasmError(Exception): ...

class WasmForeignObject:
"""WebAssembly foreign object for hybrid quantum/classical computation.

Expand Down
2 changes: 2 additions & 0 deletions python/pecos-rslib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ fn pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
// WebAssembly foreign object (optional)
#[cfg(feature = "wasm")]
m.add_class::<PyWasmForeignObject>()?;
#[cfg(feature = "wasm")]
wasm_foreign_object_bindings::register_wasm_types(m)?;

// Register namespace modules (quantum, noise, llvm) for organizational structure
// Note: This must come after all the factory functions and classes are registered
Expand Down
16 changes: 12 additions & 4 deletions python/pecos-rslib/src/wasm_foreign_object_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use pyo3::prelude::*;
use pyo3::types::PyBytes;
use std::path::Path;

pyo3::create_exception!(pecos_rslib, WasmError, pyo3::exceptions::PyException);

/// Python wrapper for `WasmForeignObject`
///
/// This class provides WebAssembly execution capabilities using the Rust
Expand Down Expand Up @@ -282,7 +284,7 @@ impl PyWasmForeignObject {
fn init(&mut self) -> PyResult<()> {
self.inner
.init()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to initialize WASM: {e}")))
.map_err(|e| PyErr::new::<WasmError, _>(format!("Failed to initialize WASM: {e}")))
}

/// Reset variables before each shot
Expand All @@ -295,7 +297,7 @@ impl PyWasmForeignObject {
fn shot_reinit(&mut self) -> PyResult<()> {
self.inner
.shot_reinit()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to call shot_reinit: {e}")))
.map_err(|e| PyErr::new::<WasmError, _>(format!("Failed to call shot_reinit: {e}")))
}

/// Create a new WASM instance
Expand All @@ -307,7 +309,7 @@ impl PyWasmForeignObject {
fn new_instance(&mut self) -> PyResult<()> {
self.inner
.new_instance()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create new instance: {e}")))
.map_err(|e| PyErr::new::<WasmError, _>(format!("Failed to create new instance: {e}")))
}

/// Get list of exported function names
Expand All @@ -332,7 +334,7 @@ impl PyWasmForeignObject {
#[allow(clippy::needless_pass_by_value)] // PyO3 extracts Python sequences as Vec
fn exec(&mut self, py: Python<'_>, func_name: &str, args: Vec<i64>) -> PyResult<Py<PyAny>> {
let results = self.inner.exec(func_name, &args).map_err(|e| {
PyRuntimeError::new_err(format!("Failed to execute '{func_name}': {e}"))
PyErr::new::<WasmError, _>(format!("Failed to execute '{func_name}': {e}"))
})?;

// Convert Vec<i64> to Python - single value as int, multiple as tuple
Expand Down Expand Up @@ -437,6 +439,12 @@ impl PyWasmForeignObject {
}
}

pub fn register_wasm_types(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
let py = parent_module.py();
parent_module.add("WasmError", py.get_type::<WasmError>())?;
Ok(())
}

impl Drop for PyWasmForeignObject {
fn drop(&mut self) {
// Ensure teardown is called when the object is dropped
Expand Down
78 changes: 78 additions & 0 deletions python/pecos-rslib/tests/test_wasm_foreign_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Tests for WasmForeignObject error handling (div-by-zero, infinite loops, etc.)."""

import tempfile
import os

import pytest

from pecos_rslib import WasmError, WasmForeignObject


def _wasm_from_wat(wat_content: str, **kwargs) -> WasmForeignObject:
"""Helper: write WAT to a temp file and create a WasmForeignObject."""
with tempfile.NamedTemporaryFile(suffix=".wat", delete=False, mode="w") as f:
f.write(wat_content)
path = f.name
try:
obj = WasmForeignObject.from_file(path, **kwargs)
finally:
os.remove(path)
return obj


def test_div_by_zero():
"""Division by zero in WASM should raise WasmError."""
wat = """
(module
(func $init (export "init"))
(func $div_by_zero (export "div_by_zero") (result i32)
i32.const 1
i32.const 0
i32.div_s
)
)
"""
wasm = _wasm_from_wat(wat)
wasm.init()

with pytest.raises(WasmError, match="[Dd]ivision by zero"):
wasm.exec("div_by_zero", [])


def test_infinite_loop():
"""An infinite loop should raise WasmError due to timeout."""
wat = """
(module
(func $init (export "init"))
(func $infinite_loop (export "infinite_loop") (result i32)
(loop $loop
br $loop
)
i32.const 0
)
)
"""
wasm = _wasm_from_wat(wat, timeout=0.2)
wasm.init()

with pytest.raises(WasmError, match="timed out"):
wasm.exec("infinite_loop", [])


def test_normal_execution_no_error():
"""Normal WASM execution should not raise any error."""
wat = """
(module
(func $init (export "init"))
(func $add (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
)
"""
wasm = _wasm_from_wat(wat)
wasm.init()

result = wasm.exec("add", [3, 4])
assert result == 7
2 changes: 2 additions & 0 deletions python/quantum-pecos/src/pecos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Pauli, # Quantum Pauli operators (I, X, Y, Z)
PauliString, # Multi-qubit Pauli operators
TimeUnits, # Abstract time duration in arbitrary units
WasmError, # WASM execution error (div-by-zero, timeout, etc.)
WasmForeignObject, # WASM foreign object for classical coprocessor
abs, # Absolute value # noqa: A004
all, # All elements true # noqa: A004
Expand Down Expand Up @@ -281,6 +282,7 @@
"TimeUnits", # Time unit type
"UnsignedInteger",
"Wasm",
"WasmError",
"WasmForeignObject",
"Wat",
# Version
Expand Down
23 changes: 21 additions & 2 deletions python/quantum-pecos/src/pecos/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@

import re

# Import Rust-defined WasmError so Python WasmError can inherit from it.
# This allows catching either pecos_rslib.WasmError or pecos.exceptions.WasmError.
try:
from pecos_rslib import WasmError as _RsWasmError
except ImportError:
_RsWasmError = None


class PECOSError(Exception):
"""Base exception raised by PECOS."""
Expand Down Expand Up @@ -70,8 +77,20 @@ class QECCError(PECOSError):
"""Error in quantum error correcting code operations."""


class WasmError(PECOSError):
"""Base WASM-related exception type."""
# WasmError inherits from both PECOSError and the Rust-defined WasmError (when available).
# This means:
# - Errors raised by Rust (pecos_rslib.WasmError) are catchable as pecos_rslib.WasmError
# - Errors raised by Python (pecos.exceptions.WasmError) are catchable as both
# PECOSError and pecos_rslib.WasmError
if _RsWasmError is not None:

class WasmError(PECOSError, _RsWasmError): # type: ignore[misc]
"""Base WASM-related exception type."""

else:

class WasmError(PECOSError): # type: ignore[no-redef]
"""Base WASM-related exception type."""


class MissingCCOPError(WasmError):
Expand Down
Loading
Loading