diff --git a/crates/stdlib/src/csv.rs b/crates/stdlib/src/csv.rs index bf094d37f06..0eee41fe4bc 100644 --- a/crates/stdlib/src/csv.rs +++ b/crates/stdlib/src/csv.rs @@ -7,6 +7,7 @@ mod _csv { AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, builtins::{PyBaseExceptionRef, PyInt, PyStr, PyType, PyTypeRef, PyUtf8StrRef}, + convert::PyListOf, function::{ArgIterable, ArgumentError, FromArgs, FuncArgs, OptionalArg, Param}, protocol::{PyIter, PyIterReturn, PyNumber}, types::{Callable, Constructor, IterNext, Iterable, SelfIter}, @@ -244,14 +245,9 @@ mod _csv { } #[pyfunction] - fn list_dialects(vm: &VirtualMachine) -> rustpython_vm::builtins::PyListRef { + fn list_dialects(vm: &VirtualMachine) -> PyListOf { let names = GLOBAL_HASHMAP.lock().keys().cloned().collect_vec(); - let t = names - .into_iter() - .map(|x| vm.ctx.new_str(x).into()) - .collect_vec(); - // .iter().map(|x| vm.ctx.new_str(x.clone()).into_pyobject(vm)).collect_vec(); - vm.ctx.new_list(t) + PyListOf::new(names, vm) } #[derive(FromArgs)] diff --git a/crates/vm/src/convert/dict.rs b/crates/vm/src/convert/dict.rs new file mode 100644 index 00000000000..1242cdba45c --- /dev/null +++ b/crates/vm/src/convert/dict.rs @@ -0,0 +1,118 @@ +use core::{fmt, marker::PhantomData}; + +use crate::{ + PyObjectRef, PyResult, VirtualMachine, + builtins::PyDictRef, + object::{Traverse, TraverseFn}, +}; + +use super::ToPyObject; + +/// A Python dict constructed from Rust key/value pairs. +/// +/// Keys use Python hashing and equality; later equal keys replace earlier values. +/// Construction can execute Python code and propagates its exceptions. Cloning +/// this handle shares the same dictionary, including subsequent mutations. +/// +/// `K` and `V` describe the input conversion, not runtime invariants: Python can +/// insert other types. Access through [`Self::as_dict`] is therefore untyped. +pub struct PyDictOf { + dict: PyDictRef, + types: PhantomData (K, V)>, +} + +impl PyDictOf { + pub fn try_new(items: impl IntoIterator, vm: &VirtualMachine) -> PyResult { + let dict = vm.ctx.new_dict(); + for (key, value) in items { + let key = key.to_pyobject(vm); + let value = value.to_pyobject(vm); + dict.set_item(&*key, value, vm)?; + } + Ok(Self { + dict, + types: PhantomData, + }) + } +} + +impl PyDictOf { + #[must_use] + pub fn as_dict(&self) -> &PyDictRef { + &self.dict + } + + #[must_use] + pub fn into_dict(self) -> PyDictRef { + self.dict + } +} + +impl Clone for PyDictOf { + fn clone(&self) -> Self { + Self { + dict: self.dict.clone(), + types: PhantomData, + } + } +} + +impl fmt::Debug for PyDictOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.dict.fmt(f) + } +} + +impl ToPyObject for PyDictOf { + fn to_pyobject(self, _vm: &VirtualMachine) -> PyObjectRef { + self.dict.into() + } +} + +impl From> for PyObjectRef { + fn from(value: PyDictOf) -> Self { + value.dict.into() + } +} + +// SAFETY: Only `dict` owns a Python reference; visit it once without cloning. +unsafe impl Traverse for PyDictOf { + fn traverse(&self, tracer: &mut TraverseFn<'_>) { + self.dict.traverse(tracer); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AsObject, Interpreter}; + + #[test] + fn pairs_use_python_key_semantics_and_propagate_errors() { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + let first: PyObjectRef = vm.ctx.new_bool(true).into(); + let dict = PyDictOf::try_new([(first.clone(), 10), (vm.ctx.new_int(1).into(), 20)], vm) + .unwrap(); + let keys = dict.as_dict().keys_vec(); + assert_eq!(keys.len(), 1); + assert!(keys[0].is(&first)); + assert_eq!( + dict.as_dict() + .get_item(&*first, vm) + .unwrap() + .try_into_value::(vm) + .unwrap(), + 20 + ); + assert!(dict.clone().to_pyobject(vm).is(dict.as_dict())); + let mut visits = 0; + dict.traverse(&mut |obj| { + assert!(obj.is(dict.as_dict())); + visits += 1; + }); + assert_eq!(visits, 1); + let error = PyDictOf::try_new([(vm.ctx.new_list(vec![]), 1)], vm).unwrap_err(); + assert!(error.fast_isinstance(vm.ctx.exceptions.type_error)); + }); + } +} diff --git a/crates/vm/src/convert/list.rs b/crates/vm/src/convert/list.rs new file mode 100644 index 00000000000..169b475079c --- /dev/null +++ b/crates/vm/src/convert/list.rs @@ -0,0 +1,134 @@ +use core::{fmt, marker::PhantomData}; + +use crate::{ + PyObjectRef, VirtualMachine, + builtins::PyListRef, + object::{Traverse, TraverseFn}, +}; + +use super::ToPyObject; + +/// A Python list constructed from Rust values of type `T`. +/// +/// Unlike `Vec`, which converts to Python `bytes`, `PyListOf` produces +/// a list of integers. Construction converts each element once; cloning this +/// handle shares the same Python list, including subsequent mutations. +/// +/// `T` describes the input conversion, not a runtime invariant: Python can add +/// objects of any type. Access through [`Self::as_list`] is therefore untyped. +pub struct PyListOf { + list: PyListRef, + element: PhantomData T>, +} + +impl PyListOf { + pub fn new(items: impl IntoIterator, vm: &VirtualMachine) -> Self { + Self { + list: vm + .ctx + .new_list(items.into_iter().map(|item| item.to_pyobject(vm)).collect()), + element: PhantomData, + } + } +} + +impl PyListOf { + #[must_use] + pub fn as_list(&self) -> &PyListRef { + &self.list + } + + #[must_use] + pub fn into_list(self) -> PyListRef { + self.list + } +} + +impl Clone for PyListOf { + fn clone(&self) -> Self { + Self { + list: self.list.clone(), + element: PhantomData, + } + } +} + +impl fmt::Debug for PyListOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.list.fmt(f) + } +} + +impl ToPyObject for PyListOf { + fn to_pyobject(self, _vm: &VirtualMachine) -> PyObjectRef { + self.list.into() + } +} + +impl From> for PyObjectRef { + fn from(value: PyListOf) -> Self { + value.list.into() + } +} + +// SAFETY: Only `list` owns a Python reference; visit it once without cloning. +unsafe impl Traverse for PyListOf { + fn traverse(&self, tracer: &mut TraverseFn<'_>) { + self.list.traverse(tracer); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AsObject, Interpreter, builtins::PyBytes}; + + #[test] + fn explicit_list_conversion() { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + let list = PyListOf::new([0_u8, 255].into_iter().filter(|_| true), vm); + assert_eq!( + list.as_list() + .as_object() + .try_to_value::>(vm) + .unwrap(), + [0, 255] + ); + assert!( + vec![0_u8, 255] + .to_pyobject(vm) + .downcast_ref::() + .is_some() + ); + assert!( + PyListOf::::new([], vm) + .as_list() + .borrow_vec() + .is_empty() + ); + }); + } + + #[test] + fn shared_identity_and_traversal_do_not_require_element_traits() { + struct Item; + impl ToPyObject for Item { + fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.new_int(1).into() + } + } + Interpreter::without_stdlib(Default::default()).enter(|vm| { + let list = PyListOf::new([Item], vm); + let copy = list.clone(); + list.as_list().borrow_vec_mut().push(vm.ctx.none()); + assert_eq!(copy.as_list().borrow_vec().len(), 2); + let mut visits = 0; + copy.traverse(&mut |obj| { + assert!(obj.is(list.as_list())); + visits += 1; + }); + assert_eq!(visits, 1); + assert!(copy.to_pyobject(vm).is(&list.into_list())); + }); + } +} diff --git a/crates/vm/src/convert/mod.rs b/crates/vm/src/convert/mod.rs index 7aed8b90321..bde052d41f5 100644 --- a/crates/vm/src/convert/mod.rs +++ b/crates/vm/src/convert/mod.rs @@ -1,9 +1,15 @@ +mod dict; mod into_object; +mod list; +mod set; mod to_pyobject; mod transmute_from; mod try_from; +pub use dict::PyDictOf; pub use into_object::IntoObject; +pub use list::PyListOf; +pub use set::PySetOf; pub use to_pyobject::{IntoPyException, ToPyException, ToPyObject, ToPyResult}; pub use transmute_from::TransmuteFromObject; pub use try_from::{TryFromBorrowedObject, TryFromObject}; diff --git a/crates/vm/src/convert/set.rs b/crates/vm/src/convert/set.rs new file mode 100644 index 00000000000..f18831a0d4e --- /dev/null +++ b/crates/vm/src/convert/set.rs @@ -0,0 +1,107 @@ +use core::{fmt, marker::PhantomData}; + +use crate::{ + PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::PySet, + object::{Traverse, TraverseFn}, +}; + +use super::ToPyObject; + +/// A Python set constructed from Rust values. +/// +/// Elements use Python hashing and equality, including duplicate elimination. +/// Construction can execute Python code and propagates its exceptions. Cloning +/// this handle shares the same set, including subsequent mutations. +/// +/// `T` describes the input conversion, not a runtime invariant: Python can add +/// other types. Access through [`Self::as_set`] is therefore untyped. +pub struct PySetOf { + set: PyRef, + types: PhantomData T>, +} + +impl PySetOf { + pub fn try_new(items: impl IntoIterator, vm: &VirtualMachine) -> PyResult { + let set = PySet::default().into_ref(&vm.ctx); + for item in items { + set.add(item.to_pyobject(vm), vm)?; + } + Ok(Self { + set, + types: PhantomData, + }) + } +} + +impl PySetOf { + #[must_use] + pub fn as_set(&self) -> &PyRef { + &self.set + } + + #[must_use] + pub fn into_set(self) -> PyRef { + self.set + } +} + +impl Clone for PySetOf { + fn clone(&self) -> Self { + Self { + set: self.set.clone(), + types: PhantomData, + } + } +} + +impl fmt::Debug for PySetOf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.set.fmt(f) + } +} + +impl ToPyObject for PySetOf { + fn to_pyobject(self, _vm: &VirtualMachine) -> PyObjectRef { + self.set.into() + } +} + +impl From> for PyObjectRef { + fn from(value: PySetOf) -> Self { + value.set.into() + } +} + +// SAFETY: Only `set` owns a Python reference; visit it once without cloning. +unsafe impl Traverse for PySetOf { + fn traverse(&self, tracer: &mut TraverseFn<'_>) { + self.set.traverse(tracer); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AsObject, Interpreter}; + + #[test] + fn elements_use_python_equality_and_propagate_errors() { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + let first: PyObjectRef = vm.ctx.new_bool(true).into(); + let set = PySetOf::try_new([first.clone(), vm.ctx.new_int(1).into()], vm).unwrap(); + let elements = set.as_set().elements(); + assert_eq!(elements.len(), 1); + assert!(elements[0].is(&first)); + assert!(set.clone().to_pyobject(vm).is(set.as_set())); + let mut visits = 0; + set.traverse(&mut |obj| { + assert!(obj.is(set.as_set())); + visits += 1; + }); + assert_eq!(visits, 1); + let error = PySetOf::try_new([vm.ctx.new_list(vec![])], vm).unwrap_err(); + assert!(error.fast_isinstance(vm.ctx.exceptions.type_error)); + }); + } +} diff --git a/crates/vm/src/stdlib/_signal.rs b/crates/vm/src/stdlib/_signal.rs index 7deec7d5d52..3980fbeabd2 100644 --- a/crates/vm/src/stdlib/_signal.rs +++ b/crates/vm/src/stdlib/_signal.rs @@ -8,6 +8,7 @@ pub(crate) mod _signal { use crate::{ Py, PyObjectRef, PyResult, VirtualMachine, + convert::PySetOf, signal::{self, SignalHandlers, SignalNum}, }; use core::sync::atomic::{self, Ordering}; @@ -463,40 +464,21 @@ pub(crate) mod _signal { } #[pyfunction] - #[cfg_attr( - not(any(unix, windows)), - expect( - clippy::unnecessary_wraps, - reason = "WASI does not support signals yet" - ) - )] - fn valid_signals(vm: &VirtualMachine) -> PyResult { - use crate::PyPayload; - use crate::builtins::PySet; - let set = PySet::default().into_ref(&vm.ctx); - - // Empty set for platforms without signal support (e.g., WASM) + fn valid_signals(vm: &VirtualMachine) -> PyResult> { #[cfg(any(unix, windows))] - for signum in host_signal::valid_signals(signal::NSIG) - .map_err(|_| vm.new_os_error("sigfillset failed"))? - { - set.add(vm.ctx.new_int(signum).into(), vm)?; - } - - Ok(set.into()) + let signals = host_signal::valid_signals(signal::NSIG) + .map_err(|_| vm.new_os_error("sigfillset failed"))?; + // Empty set for platforms without signal support (e.g., WASM) + #[cfg(not(any(unix, windows)))] + let signals = core::iter::empty(); + PySetOf::try_new(signals, vm) } #[cfg(unix)] fn sigset_to_pyset(mask: libc::sigset_t, vm: &VirtualMachine) -> PyResult { - use crate::PyPayload; - use crate::builtins::PySet; - let set = PySet::default().into_ref(&vm.ctx); - for signum in SignalNum::VALID_RANGE { - if host_signal::sigset_contains(mask, signum) { - set.add(vm.ctx.new_int(signum).into(), vm)?; - } - } - Ok(set.into()) + let signals = + SignalNum::VALID_RANGE.filter(|&signum| host_signal::sigset_contains(mask, signum)); + Ok(PySetOf::try_new(signals, vm)?.into()) } #[cfg(unix)] diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index e80e1c2d0d1..a4526bb3d1c 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -43,7 +43,7 @@ pub mod sys { ascii, hash::{PyHash, PyUHash}, }, - convert::ToPyObject, + convert::{PyDictOf, ToPyObject}, frame::FrameObjectRef, function::{FuncArgs, KwArgs, OptionalArg, PosArgs}, stdlib::{_warnings::warn, builtins}, @@ -1167,44 +1167,28 @@ pub mod sys { #[cfg(feature = "threading")] #[pyfunction] - fn _current_frames(vm: &VirtualMachine) -> PyResult { - use crate::AsObject; + fn _current_frames(vm: &VirtualMachine) -> PyResult> { use crate::stdlib::_thread::get_all_current_frames; - let frames = get_all_current_frames(vm); - let dict = vm.ctx.new_dict(); - - for (thread_id, frame) in frames { - let key = vm.ctx.new_int(thread_id); - dict.set_item(key.as_object(), frame.into(), vm)?; - } - - Ok(dict) + PyDictOf::try_new(get_all_current_frames(vm), vm) } #[cfg(feature = "threading")] #[pyfunction] - fn _current_exceptions(vm: &VirtualMachine) -> PyResult { - use crate::AsObject; + fn _current_exceptions( + vm: &VirtualMachine, + ) -> PyResult>> { use crate::vm::thread::get_all_current_exceptions; - let dict = vm.ctx.new_dict(); - for (thread_id, exc) in get_all_current_exceptions(vm) { - let key = vm.ctx.new_int(thread_id); - let value = exc.map_or_else(|| vm.ctx.none(), |e| e.into()); - dict.set_item(key.as_object(), value, vm)?; - } - - Ok(dict) + PyDictOf::try_new(get_all_current_exceptions(vm), vm) } #[cfg(not(feature = "threading"))] #[pyfunction] - fn _current_exceptions(vm: &VirtualMachine) -> PyResult { - let dict = vm.ctx.new_dict(); - let key = vm.ctx.new_int(0); - dict.set_item(key.as_object(), vm.topmost_exception().to_pyobject(vm), vm)?; - Ok(dict) + fn _current_exceptions( + vm: &VirtualMachine, + ) -> PyResult>> { + PyDictOf::try_new([(0, vm.topmost_exception())], vm) } // Stub for non-threading builds - returns empty dict diff --git a/examples/call_between_rust_and_python.py b/examples/call_between_rust_and_python.py index 4a52e85eac2..c4de961d91d 100644 --- a/examples/call_between_rust_and_python.py +++ b/examples/call_between_rust_and_python.py @@ -10,6 +10,7 @@ def python_callback(): python_person = PythonPerson("Peter Python") rust_object = rust_function(42, "This is a python string", python_person) print("Printing member 'numbers' from rust struct: ", rust_object.numbers) + print("Manually converted measurements: ", rust_object.measurements, flush=True) rust_object.print_in_rust_from_python() diff --git a/examples/call_between_rust_and_python.rs b/examples/call_between_rust_and_python.rs index b0e13a5e60b..1c7c4dd1dfa 100644 --- a/examples/call_between_rust_and_python.rs +++ b/examples/call_between_rust_and_python.rs @@ -1,3 +1,5 @@ +extern crate alloc; + use rustpython::InterpreterBuilderExt; use rustpython::vm::{ Py, PyObject, PyPayload, PyResult, TryFromBorrowedObject, VirtualMachine, pyclass, pymodule, @@ -26,7 +28,11 @@ pub fn main() { #[pymodule] mod rust_py_module { use super::*; - use rustpython::vm::{PyObjectRef, convert::ToPyObject}; + use alloc::collections::BTreeMap; + use rustpython::vm::{ + PyObjectRef, + convert::{PyListOf, ToPyObject}, + }; #[pyfunction] fn rust_function( @@ -43,17 +49,27 @@ python_person.name: {}", num, s, python_person.name ); RustStruct { - numbers: NumVec(vec![1, 2, 3, 4]), + numbers: vec![1, 2, 3, 4], + measurements: Measurements(BTreeMap::from([ + ("temperature", 21.5), + ("pressure", 1013.0), + ])), } } + // A domain type can choose its own Python representation. Here an ordered + // map becomes a list of pairs rather than a dictionary. #[derive(Debug, Clone)] - struct NumVec(Vec); + struct Measurements(BTreeMap<&'static str, f64>); - impl ToPyObject for NumVec { + impl ToPyObject for Measurements { fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { - let list = self.0.into_iter().map(|e| vm.new_pyobj(e)).collect(); - vm.ctx.new_list(list).to_pyobject(vm) + let pairs = self + .0 + .into_iter() + .map(|pair| pair.to_pyobject(vm)) + .collect(); + vm.ctx.new_list(pairs).into() } } @@ -61,14 +77,20 @@ python_person.name: {}", #[pyclass(module = "rust_py_module", name = "RustStruct")] #[derive(Debug, PyPayload)] struct RustStruct { - numbers: NumVec, + numbers: Vec, + measurements: Measurements, } #[pyclass] impl RustStruct { #[pygetset] - fn numbers(zelf: &Py) -> NumVec { - zelf.numbers.clone() + fn numbers(zelf: &Py, vm: &VirtualMachine) -> PyListOf { + PyListOf::new(zelf.numbers.iter().copied(), vm) + } + + #[pygetset] + fn measurements(zelf: &Py) -> Measurements { + zelf.measurements.clone() } #[pymethod]