diff --git a/derive-impl/src/pyclass.rs b/derive-impl/src/pyclass.rs index 8b1759dee..3f7881107 100644 --- a/derive-impl/src/pyclass.rs +++ b/derive-impl/src/pyclass.rs @@ -6,7 +6,7 @@ use crate::util::{ }; use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use syn::{ parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Meta, NestedMeta, Result, @@ -62,7 +62,8 @@ impl FromStr for AttrName { #[derive(Default)] struct ImplContext { - impl_extend_items: ItemNursery, + attribute_items: ItemNursery, + method_items: MethodNursery, getset_items: GetSetNursery, member_items: MemberNursery, extend_slots_items: ItemNursery, @@ -92,6 +93,7 @@ fn extract_items_into_context<'a, Item>( }); context.errors.ok_or_push(r); } + context.errors.ok_or_push(context.method_items.validate()); context.errors.ok_or_push(context.getset_items.validate()); context.errors.ok_or_push(context.member_items.validate()); } @@ -157,18 +159,28 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result, + ) { + #method_def + } + }, parse_quote! { fn __extend_py_class( ctx: &::rustpython_vm::Context, @@ -202,6 +214,14 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result, + ) { + #impl_ty::__extend_method_def(method_defs); + #with_method_defs + } + fn extend_slots(slots: &mut ::rustpython_vm::types::PyTypeSlots) { #impl_ty::__extend_slots(slots); #with_slots @@ -235,9 +255,10 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result Result, + ) { + #method_def + } + }, parse_quote! { fn __extend_py_class( ctx: &::rustpython_vm::Context, @@ -732,51 +761,14 @@ where let py_name = item_meta.method_name()?; let sig_doc = text_signature(func.sig(), &py_name); - let tokens = { - let doc = args.attrs.doc().map_or_else(TokenStream::new, |mut doc| { - doc = format_doc(&sig_doc, &doc); - quote!(.with_doc(#doc.to_owned(), ctx)) - }); - let build_func = match self.inner.attr_name { - AttrName::Method => quote!(.build_method(ctx, class, false)), - AttrName::ClassMethod => quote!(.build_classmethod(ctx, class)), - AttrName::StaticMethod => quote!(.build_staticmethod(ctx, class)), - other => unreachable!( - "Only 'method', 'classmethod' and 'staticmethod' are supported, got {:?}", - other - ), - }; - if py_name.starts_with("__") && py_name.ends_with("__") { - let name_ident = Ident::new(&py_name, ident.span()); - quote_spanned! { ident.span() => - class.set_attr( - ctx.names.#name_ident, - ctx.make_func_def(ctx.intern_str(#py_name), Self::#ident) - #doc - #build_func - .into(), - ); - } - } else { - quote_spanned! { ident.span() => - class.set_str_attr( - #py_name, - ctx.make_func_def(ctx.intern_str(#py_name), Self::#ident) - #doc - #build_func, - ctx, - ); - } - } - }; - - args.context.impl_extend_items.add_item( - ident.clone(), - vec![py_name], - args.cfgs.to_vec(), - tokens, - 5, - )?; + let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc)); + args.context.method_items.add_item(MethodNurseryItem { + py_name, + cfgs: args.cfgs.to_vec(), + ident: ident.to_owned(), + doc, + attr_name: self.inner.attr_name, + }); Ok(()) } } @@ -898,7 +890,7 @@ where }; args.context - .impl_extend_items + .attribute_items .add_item(ident.clone(), vec![py_name], cfgs, tokens, 1)?; Ok(()) @@ -960,6 +952,78 @@ where } } +#[derive(Default)] +struct MethodNursery { + items: Vec, +} + +struct MethodNurseryItem { + py_name: String, + cfgs: Vec, + ident: Ident, + doc: Option, + attr_name: AttrName, +} + +impl MethodNursery { + fn add_item(&mut self, item: MethodNurseryItem) { + self.items.push(item); + } + + fn validate(&mut self) -> Result<()> { + let mut name_set = HashSet::new(); + for item in &self.items { + if !name_set.insert((&item.py_name, &item.cfgs)) { + bail_span!(item.ident, "duplicate method name `{}`", item.py_name); + } + } + Ok(()) + } +} + +impl ToTokens for MethodNursery { + fn to_tokens(&self, tokens: &mut TokenStream) { + for item in &self.items { + let py_name = &item.py_name; + let ident = &item.ident; + let cfgs = &item.cfgs; + let doc = if let Some(doc) = item.doc.as_ref() { + quote! { Some(#doc) } + } else { + quote! { None } + }; + let flags = match &item.attr_name { + AttrName::Method => { + quote! { rustpython_vm::function::PyMethodFlags::METHOD } + } + AttrName::ClassMethod => { + quote! { rustpython_vm::function::PyMethodFlags::CLASS } + } + AttrName::StaticMethod => { + quote! { rustpython_vm::function::PyMethodFlags::STATIC } + } + _ => unreachable!(), + }; + // TODO: intern + // let py_name = if py_name.starts_with("__") && py_name.ends_with("__") { + // let name_ident = Ident::new(&py_name, ident.span()); + // quote_spanned! { ident.span() => ctx.names.#name_ident } + // } else { + // quote_spanned! { ident.span() => #py_name } + // }; + tokens.extend(quote! { + #(#cfgs)* + method_defs.push(rustpython_vm::function::PyMethodDef { + name: #py_name, + func: rustpython_vm::function::IntoPyNativeFn::into_func(Self::#ident), + flags: #flags, + doc: #doc, + }); + }); + } + } +} + #[derive(Default)] #[allow(clippy::type_complexity)] struct GetSetNursery { @@ -1367,13 +1431,15 @@ impl MemberItemMeta { struct ExtractedImplAttrs { payload: Option, - with_impl: TokenStream, - with_slots: TokenStream, flags: TokenStream, + with_impl: TokenStream, + with_method_defs: TokenStream, + with_slots: TokenStream, } fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result { let mut withs = Vec::new(); + let mut with_method_defs = Vec::new(); let mut with_slots = Vec::new(); let mut flags = vec![quote! { { @@ -1396,16 +1462,18 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result::__extend_py_class), + quote!(#path::::__extend_method_def), quote!(#path::::__extend_slots), ) } else { ( quote!(::__extend_py_class), + quote!(::__extend_method_def), quote!(::__extend_slots), ) }; @@ -1413,6 +1481,9 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result #extend_class(ctx, class); }); + with_method_defs.push(quote_spanned! { path.span() => + #extend_method_def(method_defs); + }); with_slots.push(quote_spanned! { item_span => #extend_slots(slots); }); @@ -1450,11 +1521,14 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result Result &'static ::rustpython_vm::builtins::PyModuleDef { DEF.get_or_init(|| { + #[allow(clippy::ptr_arg)] + let method_defs = { + let mut method_defs = Vec::new(); + extend_method_def(ctx, &mut method_defs); + method_defs + }; let mut def = ::rustpython_vm::builtins::PyModuleDef { name: ctx.intern_str(MODULE_NAME), doc: DOC.map(|doc| ctx.intern_str(doc)), + methods: Box::leak(method_defs.into_boxed_slice()), slots: Default::default(), }; def.slots.exec = Some(extend_module); @@ -160,6 +167,18 @@ pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result, + ) { + #( + super::#withs::extend_method_def(ctx, method_defs); + )* + #function_items + } + }, parse_quote! { pub(crate) fn __init_attributes( vm: &::rustpython_vm::VirtualMachine, @@ -177,23 +196,10 @@ pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result, ) { - __init_methods(vm, module); + module.__init_methods(vm).unwrap(); __init_attributes(vm, module); } }, - parse_quote! { - // TODO: remove once PyMethodDef done - pub(crate) fn __init_methods( - vm: &::rustpython_vm::VirtualMachine, - module: &::rustpython_vm::Py<::rustpython_vm::builtins::PyModule>, - ) { - #( - super::#withs::__init_methods(vm, module); - )* - let ctx = &vm.ctx; - #function_items - } - }, parse_quote! { pub(crate) fn __init_dict( vm: &::rustpython_vm::VirtualMachine, @@ -330,9 +336,7 @@ struct FunctionNurseryItem { py_names: Vec, cfgs: Vec, ident: Ident, - #[allow(dead_code)] doc: String, - tokens: TokenStream, } impl FunctionNursery { @@ -358,11 +362,23 @@ struct ValidatedFunctionNursery(FunctionNursery); impl ToTokens for ValidatedFunctionNursery { fn to_tokens(&self, tokens: &mut TokenStream) { for item in &self.0.items { + let ident = &item.ident; let cfgs = &item.cfgs; - let item_tokens = &item.tokens; + let py_names = &item.py_names; + let doc = &item.doc; + let flags = quote! { rustpython_vm::function::PyMethodFlags::empty() }; + tokens.extend(quote! { #(#cfgs)* - #item_tokens + { + let doc = Some(#doc); + #(method_defs.push(rustpython_vm::function::PyMethodDef::new( + (#py_names), + #ident, + #flags, + doc, + ));)* + } }); } } @@ -438,38 +454,23 @@ impl ModuleItem for FunctionItem { let py_name = item_meta.simple_name()?; let sig_doc = text_signature(func.sig(), &py_name); - let (tokens, py_names, doc) = { - let module = args.module_name(); - let doc = args.attrs.doc().or_else(|| { - crate::doc::Database::shared() - .try_module_item(module, &py_name) - .ok() // TODO: doc must exist at least one of code or CPython - .flatten() - .map(str::to_owned) - }); - let doc = if let Some(doc) = doc { - format_doc(&sig_doc, &doc) - } else { - sig_doc - }; - let with_doc = quote!(.with_doc(#doc.to_owned(), &vm.ctx)); - let new_func = quote_spanned!(ident.span()=> - vm.ctx.make_func_def(vm.ctx.intern_str(#py_name), #ident) - #with_doc - .into_function() - .with_module(vm.new_pyobj(#module.to_owned())) - .into_ref(&vm.ctx) - ); + let module = args.module_name(); + let doc = args.attrs.doc().or_else(|| { + crate::doc::Database::shared() + .try_module_item(module, &py_name) + .ok() // TODO: doc must exist at least one of code or CPython + .flatten() + .map(str::to_owned) + }); + let doc = if let Some(doc) = doc { + format_doc(&sig_doc, &doc) + } else { + sig_doc + }; + let py_names = { if self.py_attrs.is_empty() { - ( - quote_spanned! { ident.span() => { - let func = #new_func; - vm.__module_set_attr(module, #py_name, func).unwrap(); - }}, - vec![py_name], - doc, - ) + vec![py_name] } else { let mut py_names = HashSet::new(); py_names.insert(py_name); @@ -494,16 +495,7 @@ impl ModuleItem for FunctionItem { args.context.errors.ok_or_push(r); } let py_names: Vec<_> = py_names.into_iter().collect(); - ( - quote_spanned! { ident.span().resolved_at(Span::call_site()) => { - let func = #new_func; - for name in [#(#py_names,)*] { - vm.__module_set_attr(module, name, func.clone()).unwrap(); - } - }}, - py_names, - doc, - ) + py_names } }; @@ -512,7 +504,6 @@ impl ModuleItem for FunctionItem { py_names, cfgs: args.cfgs.to_vec(), doc, - tokens, }); Ok(()) } @@ -585,12 +576,12 @@ impl ModuleItem for ClassItem { 1 => { let py_name = &py_names[0]; quote! { - vm.__module_set_attr(&module, #py_name, new_class).unwrap(); + vm.__module_set_attr(&module, vm.ctx.intern_str(#py_name), new_class).unwrap(); } } _ => quote! { for name in [#(#py_names,)*] { - vm.__module_set_attr(&module, name, new_class.clone()).unwrap(); + vm.__module_set_attr(&module, vm.ctx.intern_str(name), new_class.clone()).unwrap(); } }, }; @@ -681,7 +672,7 @@ impl ModuleItem for AttributeItem { ident.to_string() }; let tokens = quote_spanned! { ident.span() => - vm.__module_set_attr(module, #py_name, vm.new_pyobj(#ident)).unwrap(); + vm.__module_set_attr(module, vm.ctx.intern_str(#py_name), vm.new_pyobj(#ident)).unwrap(); }; args.context.attribute_items.add_item( ident.clone(), @@ -705,7 +696,7 @@ impl ModuleItem for AttributeItem { ( quote_spanned! { ident.span() => { #let_obj - vm.__module_set_attr(module, #py_name, obj).unwrap(); + vm.__module_set_attr(module, vm.ctx.intern_str(#py_name), obj).unwrap(); }}, vec![py_name], ) @@ -738,7 +729,7 @@ impl ModuleItem for AttributeItem { quote_spanned! { ident.span() => { #let_obj for name in [(#(#names,)*)] { - vm.__module_set_attr(module, name, obj.clone()).unwrap(); + vm.__module_set_attr(module, vm.ctx.intern_str(name), obj.clone()).unwrap(); } }}, names, diff --git a/derive-impl/src/util.rs b/derive-impl/src/util.rs index c0979f080..c7b98ab2c 100644 --- a/derive-impl/src/util.rs +++ b/derive-impl/src/util.rs @@ -320,6 +320,7 @@ impl ItemMeta for ClassItemMeta { "base", "metaclass", "unhashable", + "ctx", "impl", "traverse", ]; diff --git a/examples/mini_repl.rs b/examples/mini_repl.rs index 4e6441951..c5c2114ac 100644 --- a/examples/mini_repl.rs +++ b/examples/mini_repl.rs @@ -41,7 +41,7 @@ fn run(vm: &vm::VirtualMachine) -> vm::PyResult<()> { // typing `quit()` is too long, let's make `on(False)` work instead. scope .globals - .set_item("on", vm.ctx.new_function("on", on).into(), vm)?; + .set_item("on", vm.new_function("on", on).into(), vm)?; // let's include a fibonacci function, but let's be lazy and write it in Python add_python_function!( diff --git a/extra_tests/snippets/builtin_type.py b/extra_tests/snippets/builtin_type.py index 68ef65efd..9f30b9b0e 100644 --- a/extra_tests/snippets/builtin_type.py +++ b/extra_tests/snippets/builtin_type.py @@ -1,3 +1,4 @@ +import types from testutils import assert_raises @@ -256,7 +257,7 @@ assert dict.fromkeys.__qualname__ == 'dict.fromkeys' assert object.__init_subclass__.__qualname__ == 'object.__init_subclass__' # Dynamic with `#[extend_class]`: -assert bytearray.maketrans.__qualname__ == 'bytearray.maketrans' +assert bytearray.maketrans.__qualname__ == 'bytearray.maketrans', bytearray.maketrans.__qualname__ # Third-party: @@ -560,3 +561,7 @@ def my_repr_func(): pass assert repr(my_repr_func).startswith(', -} - -impl PyNativeFuncDef { - pub fn new(func: &'static PyNativeFunc, name: &'static PyStrInterned) -> Self { - Self { - func, - name, - doc: None, - } - } - - pub fn with_doc(mut self, doc: String, ctx: &Context) -> Self { - self.doc = Some(PyStr::new_ref(doc, ctx)); - self - } - - pub fn into_function(self) -> PyBuiltinFunction { - PyBuiltinFunction { - zelf: None, - value: self, - module: None, - is_classmethod: false, - } - } - pub fn into_method(self, obj: PyObjectRef, is_classmethod: bool) -> PyBuiltinFunction { - PyBuiltinFunction { - zelf: Some(obj), - value: self, - module: None, - is_classmethod, - } - } - pub fn build_function(self, ctx: &Context) -> PyRef { - self.into_function().into_ref(ctx) - } - pub fn build_method( - self, - ctx: &Context, - class: &'static Py, - is_classmethod: bool, - ) -> PyRef { - PyRef::new_ref( - PyBuiltinMethod { - value: self, - class, - is_classmethod, - }, - ctx.types.method_descriptor_type.to_owned(), - None, - ) - } - pub fn build_classmethod( - self, - ctx: &Context, - class: &'static Py, - ) -> PyRef { - // TODO: classmethod_descriptor - let callable = self.build_method(ctx, class, true).into(); - PyClassMethod::new_ref(callable, ctx) - } - pub fn build_staticmethod( - self, - ctx: &Context, - class: &'static Py, - ) -> PyRef { - // TODO - let callable = self.build_method(ctx, class, true).into(); - PyStaticMethod::new_ref(callable, ctx) - } -} - +// PyCFunctionObject in CPython #[pyclass(name = "builtin_function_or_method", module = false)] -pub struct PyBuiltinFunction { - zelf: Option, - value: PyNativeFuncDef, - module: Option, - is_classmethod: bool, +pub struct PyNativeFunction { + pub(crate) value: &'static PyMethodDef, + pub(crate) zelf: Option, + pub(crate) module: Option<&'static PyStrInterned>, // None for bound method } -impl PyPayload for PyBuiltinFunction { +impl PyPayload for PyNativeFunction { fn class(ctx: &Context) -> &'static Py { ctx.types.builtin_function_or_method_type } } -impl fmt::Debug for PyBuiltinFunction { +impl fmt::Debug for PyNativeFunction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "builtin function {}", self.value.name.as_str()) + write!( + f, + "builtin function {}.{} ({:?}) self as instance of {:?}", + self.module.map_or("", |m| m.as_str()), + self.value.name, + self.value.flags, + self.zelf.as_ref().map(|z| z.class().name().to_owned()) + ) } } -impl PyBuiltinFunction { - pub fn with_module(mut self, module: PyObjectRef) -> Self { +impl PyNativeFunction { + pub fn with_module(mut self, module: &'static PyStrInterned) -> Self { self.module = Some(module); self } @@ -117,12 +49,12 @@ impl PyBuiltinFunction { ) } - pub fn as_func(&self) -> &'static PyNativeFunc { + pub fn as_func(&self) -> &'static PyNativeFn { self.value.func } } -impl Callable for PyBuiltinFunction { +impl Callable for PyNativeFunction { type Args = FuncArgs; #[inline] fn call(zelf: &Py, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { @@ -134,196 +66,185 @@ impl Callable for PyBuiltinFunction { } #[pyclass(with(Callable, Constructor), flags(HAS_DICT))] -impl PyBuiltinFunction { +impl PyNativeFunction { #[pygetset(magic)] - fn module(&self, vm: &VirtualMachine) -> PyObjectRef { - vm.unwrap_or_none(self.module.clone()) + fn module(zelf: NativeFunctionOrMethod) -> Option<&'static PyStrInterned> { + zelf.0.module } #[pygetset(magic)] - fn name(&self) -> PyStrRef { - self.value.name.to_owned() + fn name(zelf: NativeFunctionOrMethod) -> &'static str { + zelf.0.value.name } #[pygetset(magic)] - fn qualname(&self, vm: &VirtualMachine) -> PyStrRef { - if let Some(zelf) = &self.zelf { - // TODO: is_classmethod 이면 zelf 의 이름을 알 방법이 없나? - let prefix = if self.is_classmethod { - zelf.get_attr("__qualname__", vm) + fn qualname(zelf: NativeFunctionOrMethod, vm: &VirtualMachine) -> PyResult { + let zelf = zelf.0; + let flags = zelf.value.flags; + // if flags.contains(PyMethodFlags::CLASS) || flags.contains(PyMethodFlags::STATIC) { + let qualname = if let Some(bound) = &zelf.zelf { + let prefix = if flags.contains(PyMethodFlags::CLASS) { + bound + .get_attr("__qualname__", vm) .unwrap() .str(vm) .unwrap() .to_string() } else { - zelf.class().name().to_string() + bound.class().name().to_string() }; - PyStr::from(format!("{}.{}", prefix, &self.value.name)).into_ref(&vm.ctx) + vm.ctx.new_str(format!("{}.{}", prefix, &zelf.value.name)) } else { - self.name() - } + vm.ctx.intern_str(zelf.value.name).to_owned() + }; + Ok(qualname) } #[pygetset(magic)] - fn doc(&self) -> Option { - self.value.doc.clone() + fn doc(zelf: NativeFunctionOrMethod) -> Option<&'static str> { + zelf.0.value.doc } #[pygetset(name = "__self__")] - fn get_self(&self, vm: &VirtualMachine) -> PyObjectRef { + fn __self__(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.none() } #[pymethod(magic)] - fn reduce(&self) -> PyStrRef { + fn reduce(&self) -> &'static str { // TODO: return (getattr, (self.object, self.name)) if this is a method - self.name() + self.value.name } #[pymethod(magic)] - fn reduce_ex(&self, _ver: PyObjectRef) -> PyStrRef { - self.name() + fn reduce_ex(zelf: PyObjectRef, _ver: PyObjectRef, vm: &VirtualMachine) -> PyResult { + vm.call_special_method(&zelf, identifier!(vm, __reduce__), ()) } #[pygetset(magic)] - fn text_signature(&self) -> Option { - let doc = self.value.doc.as_ref()?; - let signature = - type_::get_text_signature_from_internal_doc(self.value.name.as_str(), doc.as_str())?; - Some(signature.to_owned()) + fn text_signature(zelf: NativeFunctionOrMethod) -> Option<&'static str> { + let doc = zelf.0.value.doc?; + let signature = type_::get_text_signature_from_internal_doc(zelf.0.value.name, doc)?; + Some(signature) } } -impl Representable for PyBuiltinFunction { +impl Representable for PyNativeFunction { #[inline] fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { Ok(format!("", zelf.value.name)) } } -impl Unconstructible for PyBuiltinFunction {} +impl Unconstructible for PyNativeFunction {} -// `PyBuiltinMethod` is similar to both `PyMethodDescrObject` in -// https://github.com/python/cpython/blob/main/Include/descrobject.h -// https://github.com/python/cpython/blob/main/Objects/descrobject.c -// and `PyCMethodObject` in -// https://github.com/python/cpython/blob/main/Include/cpython/methodobject.h -// https://github.com/python/cpython/blob/main/Objects/methodobject.c -#[pyclass(module = false, name = "method_descriptor")] -pub struct PyBuiltinMethod { - value: PyNativeFuncDef, - class: &'static Py, - is_classmethod: bool, +// `PyCMethodObject` in CPython +#[pyclass(name = "builtin_method", module = false, base = "PyNativeFunction")] +pub struct PyNativeMethod { + pub(crate) func: PyNativeFunction, + pub(crate) class: &'static Py, // TODO: the actual life is &'self } -impl PyPayload for PyBuiltinMethod { +#[pyclass(with(Callable, Comparable, Representable), flags(HAS_DICT))] +impl PyNativeMethod { + #[pygetset(magic)] + fn qualname(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let prefix = zelf.class.name().to_string(); + Ok(vm + .ctx + .new_str(format!("{}.{}", prefix, &zelf.func.value.name))) + } + + #[pymethod(magic)] + fn reduce(&self, vm: &VirtualMachine) -> PyResult<(PyObjectRef, (PyObjectRef, &'static str))> { + // TODO: return (getattr, (self.object, self.name)) if this is a method + let getattr = vm.builtins.get_attr("getattr", vm)?; + let target = self + .func + .zelf + .clone() + .unwrap_or_else(|| self.class.to_owned().into()); + let name = self.func.value.name; + Ok((getattr, (target, name))) + } + + #[pygetset(name = "__self__")] + fn __self__(zelf: PyRef, _vm: &VirtualMachine) -> Option { + zelf.func.zelf.clone() + } +} + +impl PyPayload for PyNativeMethod { fn class(ctx: &Context) -> &'static Py { - ctx.types.method_descriptor_type + ctx.types.builtin_method_type } } -impl fmt::Debug for PyBuiltinMethod { +impl fmt::Debug for PyNativeMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "method descriptor for '{}'", self.value.name) + write!( + f, + "builtin method of {:?} with {:?}", + &*self.class.name(), + &self.func + ) } } -impl GetDescriptor for PyBuiltinMethod { - fn descr_get( - zelf: PyObjectRef, - obj: Option, - cls: Option, - vm: &VirtualMachine, - ) -> PyResult { - let (_zelf, obj) = match Self::_check(&zelf, obj, vm) { - Some(obj) => obj, - None => return Ok(zelf), - }; - let r = if vm.is_none(&obj) && !Self::_cls_is(&cls, obj.class()) { - zelf - } else if _zelf.is_classmethod { - _zelf - .value - .clone() - .into_method(cls.unwrap(), _zelf.is_classmethod) - .into_ref(&vm.ctx) - .into() - } else { - _zelf - .value - .clone() - .into_method(obj, _zelf.is_classmethod) - .into_ref(&vm.ctx) - .into() - }; - Ok(r) - } -} - -impl Callable for PyBuiltinMethod { - type Args = FuncArgs; - #[inline] - fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - (zelf.value.func)(vm, args) - } -} - -impl PyBuiltinMethod { - pub fn new_ref( - name: &'static PyStrInterned, - class: &'static Py, - f: F, - ctx: &Context, - ) -> PyRef - where - F: IntoPyNativeFunc, - { - ctx.make_func_def(name, f).build_method(ctx, class, false) - } -} - -#[pyclass( - with(GetDescriptor, Callable, Constructor, Representable), - flags(METHOD_DESCR) -)] -impl PyBuiltinMethod { - #[pygetset(magic)] - fn name(&self) -> PyStrRef { - self.value.name.to_owned() - } - #[pygetset(magic)] - fn qualname(&self) -> String { - format!("{}.{}", self.class.name(), &self.value.name) - } - #[pygetset(magic)] - fn doc(&self) -> Option { - self.value.doc.clone() - } - #[pygetset(magic)] - fn text_signature(&self) -> Option { - self.value.doc.as_ref().and_then(|doc| { - type_::get_text_signature_from_internal_doc(self.value.name.as_str(), doc.as_str()) - .map(|signature| signature.to_string()) +impl Comparable for PyNativeMethod { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + _vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + if let Some(other) = other.payload::() { + let eq = match (zelf.func.zelf.as_ref(), other.func.zelf.as_ref()) { + (Some(z), Some(o)) => z.is(o), + (None, None) => true, + _ => false, + }; + let eq = eq && std::ptr::eq(zelf.func.value, other.func.value); + Ok(eq.into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } }) } - #[pymethod(magic)] - fn reduce( - &self, - vm: &VirtualMachine, - ) -> (Option, (Option, PyStrRef)) { - let builtins_getattr = vm.builtins.get_attr("getattr", vm).ok(); - let classname = vm.builtins.get_attr(&self.class.__name__(vm), vm).ok(); - (builtins_getattr, (classname, self.value.name.to_owned())) +} + +impl Callable for PyNativeMethod { + type Args = FuncArgs; + #[inline] + fn call(zelf: &Py, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if let Some(zelf) = &zelf.func.zelf { + args.prepend_arg(zelf.clone()); + } + (zelf.func.value.func)(vm, args) } } -impl Representable for PyBuiltinMethod { +impl Representable for PyNativeMethod { #[inline] fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { Ok(format!( - "", - &zelf.value.name, + "", + &zelf.func.value.name, zelf.class.name() )) } } -impl Unconstructible for PyBuiltinMethod {} +impl Unconstructible for PyNativeMethod {} pub fn init(context: &Context) { - PyBuiltinFunction::extend_class(context, context.types.builtin_function_or_method_type); - PyBuiltinMethod::extend_class(context, context.types.method_descriptor_type); + PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type); + PyNativeMethod::extend_class(context, context.types.builtin_method_type); +} + +struct NativeFunctionOrMethod(PyRef); + +impl TryFromObject for NativeFunctionOrMethod { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + let class = vm.ctx.types.builtin_function_or_method_type; + if obj.fast_isinstance(class) { + Ok(NativeFunctionOrMethod(unsafe { obj.downcast_unchecked() })) + } else { + Err(vm.new_downcast_type_error(class, &obj)) + } + } } diff --git a/vm/src/builtins/descriptor.rs b/vm/src/builtins/descriptor.rs index 2425f3ffe..eb4afbd2f 100644 --- a/vm/src/builtins/descriptor.rs +++ b/vm/src/builtins/descriptor.rs @@ -1,19 +1,155 @@ -use super::{PyStr, PyStrInterned, PyType, PyTypeRef}; +use super::{PyStr, PyStrInterned, PyType}; use crate::{ + builtins::{builtin_func::PyNativeMethod, type_}, class::PyClassImpl, - function::PySetterValue, - types::{Constructor, GetDescriptor, Representable, Unconstructible}, - AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + function::{FuncArgs, PyMethodDef, PyMethodFlags, PySetterValue}, + types::{Callable, Constructor, GetDescriptor, Representable, Unconstructible}, + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, }; use rustpython_common::lock::PyRwLock; #[derive(Debug)] -pub struct DescrObject { - pub typ: PyTypeRef, +pub struct PyDescriptor { + pub typ: &'static Py, pub name: &'static PyStrInterned, pub qualname: PyRwLock>, } +#[derive(Debug)] +pub struct PyDescriptorOwned { + pub typ: PyRef, + pub name: &'static PyStrInterned, + pub qualname: PyRwLock>, +} + +#[pyclass(name = "method_descriptor", module = false)] +pub struct PyMethodDescriptor { + pub common: PyDescriptor, + pub method: &'static PyMethodDef, + // vectorcall: vectorcallfunc, +} + +impl PyMethodDescriptor { + pub fn new(method: &'static PyMethodDef, typ: &'static Py, ctx: &Context) -> Self { + Self { + common: PyDescriptor { + typ, + name: ctx.intern_str(method.name), + qualname: PyRwLock::new(None), + }, + method, + } + } +} + +impl PyPayload for PyMethodDescriptor { + fn class(ctx: &Context) -> &'static Py { + ctx.types.method_descriptor_type + } +} + +impl std::fmt::Debug for PyMethodDescriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "method descriptor for '{}'", self.common.name) + } +} + +impl GetDescriptor for PyMethodDescriptor { + fn descr_get( + zelf: PyObjectRef, + obj: Option, + cls: Option, + vm: &VirtualMachine, + ) -> PyResult { + let descr = Self::_as_pyref(&zelf, vm).unwrap(); + let bound = match obj { + Some(obj) => { + if descr.method.flags.contains(PyMethodFlags::METHOD) { + if cls.map_or(false, |c| c.fast_isinstance(vm.ctx.types.type_type)) { + obj + } else { + return Err(vm.new_type_error(format!( + "descriptor '{}' needs a type, not '{}', as arg 2", + descr.common.name.as_str(), + obj.class().name() + ))); + } + } else if descr.method.flags.contains(PyMethodFlags::CLASS) { + obj.class().to_owned().into() + } else { + unimplemented!() + } + } + None if descr.method.flags.contains(PyMethodFlags::CLASS) => cls.unwrap(), + None => return Ok(zelf), + }; + // Ok(descr.method.build_bound_method(&vm.ctx, bound, class).into()) + Ok(descr.bind(bound, &vm.ctx).into()) + } +} + +impl Callable for PyMethodDescriptor { + type Args = FuncArgs; + #[inline] + fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + (zelf.method.func)(vm, args) + } +} + +impl PyMethodDescriptor { + pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef { + self.method.build_bound_method(ctx, obj, self.common.typ) + } +} + +#[pyclass( + with(GetDescriptor, Callable, Constructor, Representable), + flags(METHOD_DESCR) +)] +impl PyMethodDescriptor { + #[pygetset(magic)] + fn name(&self) -> &'static PyStrInterned { + self.common.name + } + #[pygetset(magic)] + fn qualname(&self) -> String { + format!("{}.{}", self.common.typ.name(), &self.common.name) + } + #[pygetset(magic)] + fn doc(&self) -> Option<&'static str> { + self.method.doc + } + #[pygetset(magic)] + fn text_signature(&self) -> Option { + self.method.doc.and_then(|doc| { + type_::get_text_signature_from_internal_doc(self.method.name, doc) + .map(|signature| signature.to_string()) + }) + } + #[pymethod(magic)] + fn reduce( + &self, + vm: &VirtualMachine, + ) -> (Option, (Option, &'static str)) { + let builtins_getattr = vm.builtins.get_attr("getattr", vm).ok(); + let classname = vm.builtins.get_attr(&self.common.typ.__name__(vm), vm).ok(); + (builtins_getattr, (classname, self.method.name)) + } +} + +impl Representable for PyMethodDescriptor { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!( + "", + &zelf.method.name, + zelf.common.typ.name() + )) + } +} + +impl Unconstructible for PyMethodDescriptor {} + #[derive(Debug)] pub enum MemberKind { Bool = 14, @@ -78,7 +214,7 @@ impl std::fmt::Debug for PyMemberDef { #[pyclass(name = "member_descriptor", module = false)] #[derive(Debug)] pub struct PyMemberDescriptor { - pub common: DescrObject, + pub common: PyDescriptorOwned, pub member: PyMemberDef, } @@ -88,8 +224,8 @@ impl PyPayload for PyMemberDescriptor { } } -fn calculate_qualname(descr: &DescrObject, vm: &VirtualMachine) -> PyResult> { - if let Some(qualname) = vm.get_attribute_opt(descr.typ.to_owned().into(), "__qualname__")? { +fn calculate_qualname(descr: &PyDescriptorOwned, vm: &VirtualMachine) -> PyResult> { + if let Some(qualname) = vm.get_attribute_opt(descr.typ.clone().into(), "__qualname__")? { let str = qualname.downcast::().map_err(|_| { vm.new_type_error( ".__objclass__.__qualname__ is not a unicode object".to_owned(), @@ -217,7 +353,7 @@ impl GetDescriptor for PyMemberDescriptor { } } -pub fn init(context: &Context) { - let member_descriptor_type = &context.types.member_descriptor_type; - PyMemberDescriptor::extend_class(context, member_descriptor_type); +pub fn init(ctx: &Context) { + PyMemberDescriptor::extend_class(ctx, ctx.types.member_descriptor_type); + PyMethodDescriptor::extend_class(ctx, ctx.types.method_descriptor_type); } diff --git a/vm/src/builtins/module.rs b/vm/src/builtins/module.rs index 4c77bc267..58174082d 100644 --- a/vm/src/builtins/module.rs +++ b/vm/src/builtins/module.rs @@ -3,7 +3,7 @@ use crate::{ builtins::{pystr::AsPyStr, PyStrInterned}, class::PyClassImpl, convert::ToPyObject, - function::FuncArgs, + function::{FuncArgs, PyMethodDef}, types::{GetAttr, Initializer, Representable}, AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, }; @@ -15,7 +15,7 @@ pub struct PyModuleDef { pub name: &'static PyStrInterned, pub doc: Option<&'static PyStrInterned>, // pub size: isize, - // pub methods: &'static [PyMethodDef], + pub methods: &'static [PyMethodDef], pub slots: PyModuleSlots, // traverse: traverseproc // clear: inquiry @@ -82,11 +82,23 @@ impl PyModule { } pub fn __init_dict_from_def(vm: &VirtualMachine, module: &Py) { let doc = module.def.unwrap().doc.map(|doc| doc.to_owned()); - module.init_module_dict(module.name.unwrap(), doc, vm); + module.init_dict(module.name.unwrap(), doc, vm); } } impl Py { + pub fn __init_methods(&self, vm: &VirtualMachine) -> PyResult<()> { + debug_assert!(self.def.is_some()); + for method in self.def.unwrap().methods { + let func = method + .to_function() + .with_module(self.name.unwrap()) + .into_ref(&vm.ctx); + vm.__module_set_attr(self, vm.ctx.intern_str(method.name), func)?; + } + Ok(()) + } + fn getattr_inner(&self, name: &Py, vm: &VirtualMachine) -> PyResult { if let Some(attr) = self.as_object().generic_getattr_opt(name, None, vm)? { return Ok(attr); @@ -115,7 +127,7 @@ impl Py { self.as_object().dict().unwrap() } // TODO: should be on PyModule, not Py - pub(crate) fn init_module_dict( + pub(crate) fn init_dict( &self, name: &'static PyStrInterned, doc: Option, @@ -176,7 +188,7 @@ impl Initializer for PyModule { .slots .flags .has_feature(crate::types::PyTypeFlags::HAS_DICT)); - zelf.init_module_dict(vm.ctx.intern_str(args.name.as_str()), args.doc, vm); + zelf.init_dict(vm.ctx.intern_str(args.name.as_str()), args.doc, vm); Ok(()) } } diff --git a/vm/src/builtins/staticmethod.rs b/vm/src/builtins/staticmethod.rs index bb99960ce..59a5b18b5 100644 --- a/vm/src/builtins/staticmethod.rs +++ b/vm/src/builtins/staticmethod.rs @@ -1,9 +1,8 @@ -use super::{PyStr, PyStrInterned, PyType, PyTypeRef}; +use super::{PyStr, PyType, PyTypeRef}; use crate::{ - builtins::builtin_func::PyBuiltinMethod, class::PyClassImpl, common::lock::PyMutex, - function::{FuncArgs, IntoPyNativeFunc}, + function::FuncArgs, types::{Callable, Constructor, GetDescriptor, Initializer, Representable}, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, }; @@ -73,27 +72,6 @@ impl PyStaticMethod { } } -impl PyStaticMethod { - pub fn new_builtin_ref( - name: &'static PyStrInterned, - class: &'static Py, - f: F, - ctx: &Context, - ) -> PyRef - where - F: IntoPyNativeFunc, - { - let callable = PyBuiltinMethod::new_ref(name, class, f, ctx).into(); - PyRef::new_ref( - Self { - callable: PyMutex::new(callable), - }, - ctx.types.staticmethod_type.to_owned(), - None, - ) - } -} - impl Initializer for PyStaticMethod { type Args = PyObjectRef; diff --git a/vm/src/builtins/super.rs b/vm/src/builtins/super.rs index 19377305e..a7eefea32 100644 --- a/vm/src/builtins/super.rs +++ b/vm/src/builtins/super.rs @@ -153,9 +153,8 @@ impl PySuper { impl GetAttr for PySuper { fn getattro(zelf: &Py, name: &Py, vm: &VirtualMachine) -> PyResult { let skip = |zelf: &Py, name| zelf.as_object().generic_getattr(name, vm); - let obj = zelf.inner.read().obj.clone(); - let (obj, start_type): (PyObjectRef, PyTypeRef) = match obj { - Some(o) => o, + let (obj, start_type): (PyObjectRef, PyTypeRef) = match &zelf.inner.read().obj { + Some(o) => o.clone(), None => return skip(zelf, name), }; // We want __class__ to return the class of the super object diff --git a/vm/src/builtins/type.rs b/vm/src/builtins/type.rs index 5aca7ed07..e0da4f7dc 100644 --- a/vm/src/builtins/type.rs +++ b/vm/src/builtins/type.rs @@ -1,11 +1,12 @@ use super::{ - mappingproxy::PyMappingProxy, object, union_, PyClassMethod, PyDictRef, PyList, PyStaticMethod, - PyStr, PyStrInterned, PyStrRef, PyTuple, PyTupleRef, PyWeak, + mappingproxy::PyMappingProxy, object, union_, PyClassMethod, PyDictRef, PyList, PyStr, + PyStrInterned, PyStrRef, PyTuple, PyTupleRef, PyWeak, }; use crate::{ builtins::{ descriptor::{ - DescrObject, MemberGetter, MemberKind, MemberSetter, PyMemberDef, PyMemberDescriptor, + MemberGetter, MemberKind, MemberSetter, PyDescriptorOwned, PyMemberDef, + PyMemberDescriptor, }, function::PyCellRef, tuple::{IntoPyTuple, PyTupleTyped}, @@ -18,7 +19,7 @@ use crate::{ lock::{PyRwLock, PyRwLockReadGuard}, }, convert::ToPyResult, - function::{FuncArgs, KwArgs, OptionalArg, PySetterValue}, + function::{FuncArgs, KwArgs, OptionalArg, PyMethodDef, PySetterValue}, identifier, object::{Traverse, TraverseFn}, protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, @@ -426,6 +427,13 @@ impl Py { pub fn iter_base_chain(&self) -> impl Iterator> { std::iter::successors(Some(self), |cls| cls.base.as_deref()) } + + pub fn extend_methods(&'static self, method_defs: &'static [PyMethodDef], ctx: &Context) { + for method_def in method_defs { + let method = method_def.to_proper_method(self, ctx); + self.set_attr(ctx.intern_str(method_def.name), method); + } + } } #[pyclass( @@ -696,11 +704,6 @@ impl PyType { }; let mut attributes = dict.to_attributes(vm); - if let Some(f) = attributes.get_mut(identifier!(vm, __new__)) { - if f.class().is(vm.ctx.types.function_type) { - *f = PyStaticMethod::from(f.clone()).into_pyobject(vm); - } - } if let Some(f) = attributes.get_mut(identifier!(vm, __init_subclass__)) { if f.class().is(vm.ctx.types.function_type) { @@ -813,7 +816,7 @@ impl PyType { }; let member_descriptor: PyRef = vm.ctx.new_pyref(PyMemberDescriptor { - common: DescrObject { + common: PyDescriptorOwned { typ: typ.clone(), name: vm.ctx.intern_str(member.as_str()), qualname: PyRwLock::new(None), @@ -977,11 +980,7 @@ fn get_signature(doc: &str) -> Option<&str> { fn find_signature<'a>(name: &str, doc: &'a str) -> Option<&'a str> { let name = name.rsplit('.').next().unwrap(); let doc = doc.strip_prefix(name)?; - if !doc.starts_with('(') { - None - } else { - Some(doc) - } + doc.starts_with('(').then_some(doc) } pub(crate) fn get_text_signature_from_internal_doc<'a>( diff --git a/vm/src/class.rs b/vm/src/class.rs index a9a00c4a8..8b10c5e62 100644 --- a/vm/src/class.rs +++ b/vm/src/class.rs @@ -1,7 +1,8 @@ //! Utilities to define a new Python class use crate::{ - builtins::{PyBaseObject, PyBoundMethod, PyType, PyTypeRef}, + builtins::{PyBaseObject, PyType, PyTypeRef}, + function::PyMethodDef, identifier, object::Py, types::{hash_not_implemented, PyTypeFlags, PyTypeSlots}, @@ -69,8 +70,6 @@ pub trait PyClassDef { pub trait PyClassImpl: PyClassDef { const TP_FLAGS: PyTypeFlags = PyTypeFlags::DEFAULT; - fn impl_extend_class(ctx: &Context, class: &'static Py); - fn extend_class(ctx: &Context, class: &'static Py) { #[cfg(debug_assertions)] { @@ -102,16 +101,21 @@ pub trait PyClassImpl: PyClassDef { ctx.new_str(module_name).into(), ); } - let bound_new = PyBoundMethod::new_ref( - class.to_owned().into(), - ctx.slot_new_wrapper.clone().into(), - ctx, - ); - class.set_attr(identifier!(ctx, __new__), bound_new.into()); + + if class.slots.new.load().is_some() { + let bound_new = Context::genesis().slot_new_wrapper.build_bound_method( + ctx, + class.to_owned().into(), + class, + ); + class.set_attr(identifier!(ctx, __new__), bound_new.into()); + } if class.slots.hash.load().map_or(0, |h| h as usize) == hash_not_implemented as usize { class.set_attr(ctx.names.__hash__, ctx.none.clone().into()); } + + class.extend_methods(class.slots.methods, ctx); } fn make_class(ctx: &Context) -> PyTypeRef @@ -130,14 +134,20 @@ pub trait PyClassImpl: PyClassDef { .to_owned() } + fn impl_extend_class(ctx: &Context, class: &'static Py); + fn impl_extend_method_def(method_defs: &mut Vec); fn extend_slots(slots: &mut PyTypeSlots); fn make_slots() -> PyTypeSlots { + let mut method_defs = Vec::new(); + Self::impl_extend_method_def(&mut method_defs); + let mut slots = PyTypeSlots { flags: Self::TP_FLAGS, name: Self::TP_NAME, basicsize: Self::BASICSIZE, doc: Self::DOC, + methods: Box::leak(method_defs.into_boxed_slice()), ..Default::default() }; diff --git a/vm/src/codecs.rs b/vm/src/codecs.rs index 29b58749f..ff7bc4891 100644 --- a/vm/src/codecs.rs +++ b/vm/src/codecs.rs @@ -2,6 +2,7 @@ use crate::{ builtins::{PyBaseExceptionRef, PyBytesRef, PyStr, PyStrRef, PyTuple, PyTupleRef}, common::{ascii, lock::PyRwLock}, convert::ToPyObject, + function::PyMethodDef, AsObject, Context, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, }; use std::{borrow::Cow, collections::HashMap, fmt::Write, ops::Range}; @@ -142,33 +143,33 @@ impl ToPyObject for PyCodec { impl CodecsRegistry { pub(crate) fn new(ctx: &Context) -> Self { + ::rustpython_vm::common::static_cell! { + static METHODS: Box<[PyMethodDef]>; + } + + let methods = METHODS.get_or_init(|| { + crate::define_methods![ + "strict_errors" => strict_errors as EMPTY, + "ignore_errors" => ignore_errors as EMPTY, + "replace_errors" => replace_errors as EMPTY, + "xmlcharrefreplace_errors" => xmlcharrefreplace_errors as EMPTY, + "backslashreplace_errors" => backslashreplace_errors as EMPTY, + "namereplace_errors" => namereplace_errors as EMPTY, + "surrogatepass_errors" => surrogatepass_errors as EMPTY, + "surrogateescape_errors" => surrogateescape_errors as EMPTY + ] + .into_boxed_slice() + }); + let errors = [ - ("strict", ctx.new_function("strict_errors", strict_errors)), - ("ignore", ctx.new_function("ignore_errors", ignore_errors)), - ( - "replace", - ctx.new_function("replace_errors", replace_errors), - ), - ( - "xmlcharrefreplace", - ctx.new_function("xmlcharrefreplace_errors", xmlcharrefreplace_errors), - ), - ( - "backslashreplace", - ctx.new_function("backslashreplace_errors", backslashreplace_errors), - ), - ( - "namereplace", - ctx.new_function("namereplace_errors", namereplace_errors), - ), - ( - "surrogatepass", - ctx.new_function("surrogatepass_errors", surrogatepass_errors), - ), - ( - "surrogateescape", - ctx.new_function("surrogateescape_errors", surrogateescape_errors), - ), + ("strict", methods[0].build_function(ctx)), + ("ignore", methods[1].build_function(ctx)), + ("replace", methods[2].build_function(ctx)), + ("xmlcharrefreplace", methods[3].build_function(ctx)), + ("backslashreplace", methods[4].build_function(ctx)), + ("namereplace", methods[5].build_function(ctx)), + ("surrogatepass", methods[6].build_function(ctx)), + ("surrogateescape", methods[7].build_function(ctx)), ]; let errors = errors .into_iter() diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 3762ee5d0..9de24bb67 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -3,8 +3,7 @@ use crate::common::{lock::PyRwLock, str::ReprOverflowError}; use crate::object::{Traverse, TraverseFn}; use crate::{ builtins::{ - traceback::PyTracebackRef, tuple::IntoPyTuple, PyNone, PyStr, PyStrRef, PyTuple, - PyTupleRef, PyType, PyTypeRef, + traceback::PyTracebackRef, PyNone, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, }, class::{PyClassImpl, StaticType}, convert::{ToPyException, ToPyObject}, @@ -767,9 +766,8 @@ impl ExceptionZoo { extend_exception!(PyLookupError, ctx, excs.lookup_error); extend_exception!(PyIndexError, ctx, excs.index_error); - extend_exception!(PyKeyError, ctx, excs.key_error, { - "__str__" => ctx.new_method(identifier!(ctx, __str__), excs.key_error, key_error_str), - }); + + extend_exception!(PyKeyError, ctx, excs.key_error); extend_exception!(PyMemoryError, ctx, excs.memory_error); extend_exception!(PyNameError, ctx, excs.name_error, { @@ -801,8 +799,6 @@ impl ExceptionZoo { "filename" => ctx.none(), // second exception filename "filename2" => ctx.none(), - "__str__" => ctx.new_method(identifier!(ctx, __str__), excs.os_error, os_error_str), - "__reduce__" => ctx.new_method(identifier!(ctx, __reduce__), excs.os_error, os_error_reduce), }); // TODO: this isn't really accurate #[cfg(windows)] @@ -895,84 +891,6 @@ fn make_arg_getter(idx: usize) -> impl Fn(PyBaseExceptionRef) -> Option PyStrRef { - let args = exc.args(); - if args.len() == 1 { - vm.exception_args_as_string(args, false) - .into_iter() - .exactly_one() - .unwrap() - } else { - exc.str(vm) - } -} - -fn os_error_str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let args = exc.args(); - let obj = exc.as_object().to_owned(); - - if args.len() == 2 { - // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic - let errno = exc.get_arg(0).unwrap().str(vm)?; - let msg = exc.get_arg(1).unwrap().str(vm)?; - - let s = match obj.get_attr("filename", vm) { - Ok(filename) => match obj.get_attr("filename2", vm) { - Ok(filename2) => format!( - "[Errno {}] {}: '{}' -> '{}'", - errno, - msg, - filename.str(vm)?, - filename2.str(vm)? - ), - Err(_) => format!("[Errno {}] {}: '{}'", errno, msg, filename.str(vm)?), - }, - Err(_) => { - format!("[Errno {errno}] {msg}") - } - }; - Ok(vm.ctx.new_str(s)) - } else { - Ok(exc.str(vm)) - } -} - -fn os_error_reduce(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef { - let args = exc.args(); - let obj = exc.as_object().to_owned(); - let mut result: Vec = vec![obj.class().to_owned().into()]; - - if args.len() >= 2 && args.len() <= 5 { - // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic - let errno = exc.get_arg(0).unwrap(); - let msg = exc.get_arg(1).unwrap(); - - if let Ok(filename) = obj.get_attr("filename", vm) { - if !vm.is_none(&filename) { - let mut args_reduced: Vec = vec![errno, msg, filename]; - - if let Ok(filename2) = obj.get_attr("filename2", vm) { - if !vm.is_none(&filename2) { - args_reduced.push(filename2); - } - } - result.push(args_reduced.into_pytuple(vm).into()); - } else { - result.push(vm.new_tuple((errno, msg)).into()); - } - } else { - result.push(vm.new_tuple((errno, msg)).into()); - } - } else { - result.push(args.into()); - } - - if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) { - result.push(dict.into()); - } - result.into_pytuple(vm) -} - fn system_exit_code(exc: PyBaseExceptionRef) -> Option { exc.args.read().first().map(|code| { match_class!(match code { @@ -1137,13 +1055,16 @@ pub(super) mod types { use crate::common::lock::PyRwLock; #[cfg_attr(target_arch = "wasm32", allow(unused_imports))] use crate::{ - builtins::{traceback::PyTracebackRef, PyInt, PyTupleRef, PyTypeRef}, + builtins::{ + traceback::PyTracebackRef, tuple::IntoPyTuple, PyInt, PyStrRef, PyTupleRef, PyTypeRef, + }, convert::ToPyResult, function::FuncArgs, types::{Constructor, Initializer}, - PyObjectRef, PyRef, PyResult, VirtualMachine, + AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine, }; use crossbeam_utils::atomic::AtomicCell; + use itertools::Itertools; // This module is designed to be used as `use builtins::*;`. // Do not add any pub symbols not included in builtins module. @@ -1275,10 +1196,26 @@ pub(super) mod types { #[derive(Debug)] pub struct PyIndexError {} - #[pyexception(name, base = "PyLookupError", ctx = "key_error", impl)] + #[pyexception(name, base = "PyLookupError", ctx = "key_error")] #[derive(Debug)] pub struct PyKeyError {} + #[pyexception] + impl PyKeyError { + #[pymethod(magic)] + fn str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyStrRef { + let args = exc.args(); + if args.len() == 1 { + vm.exception_args_as_string(args, false) + .into_iter() + .exactly_one() + .unwrap() + } else { + exc.str(vm) + } + } + } + #[pyexception(name, base = "PyException", ctx = "memory_error", impl)] #[derive(Debug)] pub struct PyMemoryError {} @@ -1347,6 +1284,74 @@ pub(super) mod types { } PyBaseException::slot_init(zelf, new_args, vm) } + + #[pymethod(magic)] + fn str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { + let args = exc.args(); + let obj = exc.as_object().to_owned(); + + if args.len() == 2 { + // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic + let errno = exc.get_arg(0).unwrap().str(vm)?; + let msg = exc.get_arg(1).unwrap().str(vm)?; + + let s = match obj.get_attr("filename", vm) { + Ok(filename) => match obj.get_attr("filename2", vm) { + Ok(filename2) => format!( + "[Errno {}] {}: '{}' -> '{}'", + errno, + msg, + filename.str(vm)?, + filename2.str(vm)? + ), + Err(_) => format!("[Errno {}] {}: '{}'", errno, msg, filename.str(vm)?), + }, + Err(_) => { + format!("[Errno {errno}] {msg}") + } + }; + Ok(vm.ctx.new_str(s)) + } else { + Ok(exc.str(vm)) + } + } + + #[pymethod(magic)] + fn reduce(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef { + let args = exc.args(); + let obj = exc.as_object().to_owned(); + let mut result: Vec = vec![obj.class().to_owned().into()]; + + if args.len() >= 2 && args.len() <= 5 { + // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic + let errno = exc.get_arg(0).unwrap(); + let msg = exc.get_arg(1).unwrap(); + + if let Ok(filename) = obj.get_attr("filename", vm) { + if !vm.is_none(&filename) { + let mut args_reduced: Vec = vec![errno, msg, filename]; + + if let Ok(filename2) = obj.get_attr("filename2", vm) { + if !vm.is_none(&filename2) { + args_reduced.push(filename2); + } + } + result.push(args_reduced.into_pytuple(vm).into()); + } else { + result.push(vm.new_tuple((errno, msg)).into()); + } + } else { + result.push(vm.new_tuple((errno, msg)).into()); + } + } else { + result.push(args.into()); + } + + if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) { + result.push(dict.into()); + } + result.into_pytuple(vm) + } } #[pyexception(name, base = "PyOSError", ctx = "blocking_io_error", impl)] diff --git a/vm/src/frame.rs b/vm/src/frame.rs index 3ec6ab788..f2fd1756b 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -1390,13 +1390,20 @@ impl ExecutingFrame<'_> { let func = self.pop_value(); let is_method = self.pop_value().is(&vm.ctx.true_value); let target = self.pop_value(); - let method = if is_method { - PyMethod::Function { target, func } + + // TODO: It was PyMethod before #4873. Check if it's correct. + let func = if is_method { + if let Some(descr_get) = func.class().mro_find_map(|cls| cls.slots.descr_get.load()) { + let cls = target.class().to_owned().into(); + descr_get(func, Some(target), Some(cls), vm)? + } else { + func + } } else { drop(target); // should be None - PyMethod::Attribute(func) + func }; - let value = method.invoke(args, vm)?; + let value = func.call(args, vm)?; self.push_value(value); Ok(None) } diff --git a/vm/src/function/builtin.rs b/vm/src/function/builtin.rs index 2bc058824..e37270bcc 100644 --- a/vm/src/function/builtin.rs +++ b/vm/src/function/builtin.rs @@ -6,7 +6,8 @@ use crate::{ use std::marker::PhantomData; /// A built-in Python function. -pub type PyNativeFunc = py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyResult); +// PyCFunction in CPython +pub type PyNativeFn = py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyResult); /// Implemented by types that are or can generate built-in functions. /// @@ -19,31 +20,30 @@ pub type PyNativeFunc = py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyResult /// For example, anything from `Fn()` to `Fn(vm: &VirtualMachine) -> u32` to /// `Fn(PyIntRef, PyIntRef) -> String` to /// `Fn(&self, PyStrRef, FooOptions, vm: &VirtualMachine) -> PyResult` -/// is `IntoPyNativeFunc`. If you do want a really general function signature, e.g. +/// is `IntoPyNativeFn`. If you do want a really general function signature, e.g. /// to forward the args to another function, you can define a function like /// `Fn(FuncArgs [, &VirtualMachine]) -> ...` /// /// Note that the `Kind` type parameter is meaningless and should be considered -/// an implementation detail; if you need to use `IntoPyNativeFunc` as a trait bound +/// an implementation detail; if you need to use `IntoPyNativeFn` as a trait bound /// just pass an unconstrained generic type, e.g. -/// `fn foo(f: F) where F: IntoPyNativeFunc` -pub trait IntoPyNativeFunc: Sized + PyThreadingConstraint + 'static { +/// `fn foo(f: F) where F: IntoPyNativeFn` +pub trait IntoPyNativeFn: Sized + PyThreadingConstraint + 'static { fn call(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult; - /// `IntoPyNativeFunc::into_func()` generates a PyNativeFunc that performs the + /// `IntoPyNativeFn::into_func()` generates a PyNativeFn that performs the /// appropriate type and arity checking, any requested conversions, and then if /// successful calls the function with the extracted parameters. - fn into_func(self) -> &'static PyNativeFunc { - Box::leak(Box::new(move |vm: &VirtualMachine, args| { - self.call(vm, args) - })) + fn into_func(self) -> &'static PyNativeFn { + let boxed = Box::new(move |vm: &VirtualMachine, args| self.call(vm, args)); + Box::leak(boxed) } } // TODO: once higher-rank trait bounds are stabilized, remove the `Kind` type -// parameter and impl for F where F: for PyNativeFuncInternal -impl IntoPyNativeFunc<(T, R, VM)> for F +// parameter and impl for F where F: for PyNativeFnInternal +impl IntoPyNativeFn<(T, R, VM)> for F where - F: PyNativeFuncInternal, + F: PyNativeFnInternal, { #[inline(always)] fn call(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult { @@ -53,11 +53,11 @@ where mod sealed { use super::*; - pub trait PyNativeFuncInternal: Sized + PyThreadingConstraint + 'static { + pub trait PyNativeFnInternal: Sized + PyThreadingConstraint + 'static { fn call_(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult; } } -use sealed::PyNativeFuncInternal; +use sealed::PyNativeFnInternal; #[doc(hidden)] pub struct OwnedParam(PhantomData); @@ -70,9 +70,9 @@ pub struct RefParam(PhantomData); // generate native python functions. // // Note that this could be done without a macro - it is simply to avoid repetition. -macro_rules! into_py_native_func_tuple { +macro_rules! into_py_native_fn_tuple { ($(($n:tt, $T:ident)),*) => { - impl PyNativeFuncInternal<($(OwnedParam<$T>,)*), R, VirtualMachine> for F + impl PyNativeFnInternal<($(OwnedParam<$T>,)*), R, VirtualMachine> for F where F: Fn($($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static, $($T: FromArgs,)* @@ -85,7 +85,7 @@ macro_rules! into_py_native_func_tuple { } } - impl PyNativeFuncInternal<(BorrowedParam, $(OwnedParam<$T>,)*), R, VirtualMachine> for F + impl PyNativeFnInternal<(BorrowedParam, $(OwnedParam<$T>,)*), R, VirtualMachine> for F where F: Fn(&Py, $($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static, S: PyPayload, @@ -99,7 +99,7 @@ macro_rules! into_py_native_func_tuple { } } - impl PyNativeFuncInternal<(RefParam, $(OwnedParam<$T>,)*), R, VirtualMachine> for F + impl PyNativeFnInternal<(RefParam, $(OwnedParam<$T>,)*), R, VirtualMachine> for F where F: Fn(&S, $($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static, S: PyPayload, @@ -113,7 +113,7 @@ macro_rules! into_py_native_func_tuple { } } - impl PyNativeFuncInternal<($(OwnedParam<$T>,)*), R, ()> for F + impl PyNativeFnInternal<($(OwnedParam<$T>,)*), R, ()> for F where F: Fn($($T,)*) -> R + PyThreadingConstraint + 'static, $($T: FromArgs,)* @@ -126,7 +126,7 @@ macro_rules! into_py_native_func_tuple { } } - impl PyNativeFuncInternal<(BorrowedParam, $(OwnedParam<$T>,)*), R, ()> for F + impl PyNativeFnInternal<(BorrowedParam, $(OwnedParam<$T>,)*), R, ()> for F where F: Fn(&Py, $($T,)*) -> R + PyThreadingConstraint + 'static, S: PyPayload, @@ -140,7 +140,7 @@ macro_rules! into_py_native_func_tuple { } } - impl PyNativeFuncInternal<(RefParam, $(OwnedParam<$T>,)*), R, ()> for F + impl PyNativeFnInternal<(RefParam, $(OwnedParam<$T>,)*), R, ()> for F where F: Fn(&S, $($T,)*) -> R + PyThreadingConstraint + 'static, S: PyPayload, @@ -156,14 +156,14 @@ macro_rules! into_py_native_func_tuple { }; } -into_py_native_func_tuple!(); -into_py_native_func_tuple!((v1, T1)); -into_py_native_func_tuple!((v1, T1), (v2, T2)); -into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3)); -into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4)); -into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5)); -into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5), (v6, T6)); -into_py_native_func_tuple!( +into_py_native_fn_tuple!(); +into_py_native_fn_tuple!((v1, T1)); +into_py_native_fn_tuple!((v1, T1), (v2, T2)); +into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3)); +into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4)); +into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5)); +into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5), (v6, T6)); +into_py_native_fn_tuple!( (v1, T1), (v2, T2), (v3, T3), @@ -178,8 +178,8 @@ mod tests { use super::*; #[test] - fn test_intonativefunc_noalloc() { - let check_zst = |f: &'static PyNativeFunc| assert_eq!(std::mem::size_of_val(f), 0); + fn test_into_native_fn_noalloc() { + let check_zst = |f: &'static PyNativeFn| assert_eq!(std::mem::size_of_val(f), 0); fn py_func(_b: bool, _vm: &crate::VirtualMachine) -> i32 { 1 } diff --git a/vm/src/function/method.rs b/vm/src/function/method.rs new file mode 100644 index 000000000..8c8146bbd --- /dev/null +++ b/vm/src/function/method.rs @@ -0,0 +1,259 @@ +use crate::{ + builtins::{ + builtin_func::{PyNativeFunction, PyNativeMethod}, + descriptor::PyMethodDescriptor, + PyType, + }, + function::{IntoPyNativeFn, PyNativeFn}, + Context, Py, PyObjectRef, PyPayload, PyRef, VirtualMachine, +}; + +bitflags::bitflags! { + // METH_XXX flags in CPython + pub struct PyMethodFlags: u32 { + // const VARARGS = 0x0001; + // const KEYWORDS = 0x0002; + // METH_NOARGS and METH_O must not be combined with the flags above. + // const NOARGS = 0x0004; + // const O = 0x0008; + + // METH_CLASS and METH_STATIC are a little different; these control + // the construction of methods for a class. These cannot be used for + // functions in modules. + const CLASS = 0x0010; + const STATIC = 0x0020; + + // METH_COEXIST allows a method to be entered even though a slot has + // already filled the entry. When defined, the flag allows a separate + // method, "__contains__" for example, to coexist with a defined + // slot like sq_contains. + // const COEXIST = 0x0040; + + // if not Py_LIMITED_API + // const FASTCALL = 0x0080; + + // This bit is preserved for Stackless Python + // const STACKLESS = 0x0100; + + // METH_METHOD means the function stores an + // additional reference to the class that defines it; + // both self and class are passed to it. + // It uses PyCMethodObject instead of PyCFunctionObject. + // May not be combined with METH_NOARGS, METH_O, METH_CLASS or METH_STATIC. + const METHOD = 0x0200; + } +} + +impl PyMethodFlags { + // FIXME: macro temp + pub const EMPTY: Self = Self::empty(); +} + +#[macro_export] +macro_rules! define_methods { + // TODO: more flexible syntax + ($($name:literal => $func:ident as $flags:ident),+) => { + vec![ $( $crate::function::PyMethodDef { + name: $name, + func: $crate::function::IntoPyNativeFn::into_func($func), + flags: $crate::function::PyMethodFlags::$flags, + doc: None, + }),+ ] + }; +} + +#[derive(Clone)] +pub struct PyMethodDef { + pub name: &'static str, // TODO: interned + pub func: &'static PyNativeFn, + pub flags: PyMethodFlags, + pub doc: Option<&'static str>, // TODO: interned +} + +impl PyMethodDef { + #[inline] + pub fn new( + name: &'static str, + func: impl IntoPyNativeFn, + flags: PyMethodFlags, + doc: Option<&'static str>, + ) -> Self { + Self { + name, + func: func.into_func(), + flags, + doc, + } + } + pub fn to_proper_method( + &'static self, + class: &'static Py, + ctx: &Context, + ) -> PyObjectRef { + if self.flags.contains(PyMethodFlags::METHOD) { + self.build_method(ctx, class).into() + } else if self.flags.contains(PyMethodFlags::CLASS) { + self.build_classmethod(ctx, class).into() + } else if self.flags.contains(PyMethodFlags::STATIC) { + self.build_staticmethod(ctx, class).into() + } else { + unreachable!(); + } + } + pub fn to_function(&'static self) -> PyNativeFunction { + PyNativeFunction { + zelf: None, + value: self, + module: None, + } + } + pub fn to_method( + &'static self, + class: &'static Py, + ctx: &Context, + ) -> PyMethodDescriptor { + PyMethodDescriptor::new(self, class, ctx) + } + pub fn to_bound_method( + &'static self, + obj: PyObjectRef, + class: &'static Py, + ) -> PyNativeMethod { + PyNativeMethod { + func: PyNativeFunction { + zelf: Some(obj), + value: self, + module: None, + }, + class, + } + } + pub fn build_function(&'static self, ctx: &Context) -> PyRef { + self.to_function().into_ref(ctx) + } + pub fn build_bound_function( + &'static self, + ctx: &Context, + obj: PyObjectRef, + ) -> PyRef { + let function = PyNativeFunction { + zelf: Some(obj), + value: self, + module: None, + }; + PyRef::new_ref( + function, + ctx.types.builtin_function_or_method_type.to_owned(), + None, + ) + } + pub fn build_method( + &'static self, + ctx: &Context, + class: &'static Py, + ) -> PyRef { + debug_assert!(self.flags.contains(PyMethodFlags::METHOD)); + PyRef::new_ref( + self.to_method(class, ctx), + ctx.types.method_descriptor_type.to_owned(), + None, + ) + } + pub fn build_bound_method( + &'static self, + ctx: &Context, + obj: PyObjectRef, + class: &'static Py, + ) -> PyRef { + PyRef::new_ref( + self.to_bound_method(obj, class), + ctx.types.builtin_method_type.to_owned(), + None, + ) + } + pub fn build_classmethod( + &'static self, + ctx: &Context, + class: &'static Py, + ) -> PyRef { + PyRef::new_ref( + self.to_method(class, ctx), + ctx.types.method_descriptor_type.to_owned(), + None, + ) + } + pub fn build_staticmethod( + &'static self, + ctx: &Context, + class: &'static Py, + ) -> PyRef { + debug_assert!(self.flags.contains(PyMethodFlags::STATIC)); + let func = self.to_function(); + PyNativeMethod { func, class }.into_ref(ctx) + } +} + +impl std::fmt::Debug for PyMethodDef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PyMethodDef") + .field("name", &self.name) + .field( + "func", + &(unsafe { std::mem::transmute::<_, [usize; 2]>(self.func)[1] as *const u8 }), + ) + .field("flags", &self.flags) + .field("doc", &self.doc) + .finish() + } +} + +// This is not a part of CPython API. +// But useful to support dynamically generated methods +#[pyclass(name, module = false, ctx = "method_def")] +#[derive(Debug)] +pub struct HeapMethodDef { + method: PyMethodDef, +} + +impl HeapMethodDef { + pub fn new(method: PyMethodDef) -> Self { + Self { method } + } +} + +impl Py { + pub(crate) unsafe fn method(&self) -> &'static PyMethodDef { + &*(&self.method as *const _) + } + + pub fn build_function(&self, vm: &VirtualMachine) -> PyRef { + let function = unsafe { self.method() }.to_function(); + let dict = vm.ctx.new_dict(); + dict.set_item("__method_def__", self.to_owned().into(), vm) + .unwrap(); + PyRef::new_ref( + function, + vm.ctx.types.builtin_function_or_method_type.to_owned(), + Some(dict), + ) + } + + pub fn build_method( + &self, + class: &'static Py, + vm: &VirtualMachine, + ) -> PyRef { + let function = unsafe { self.method() }.to_method(class, &vm.ctx); + let dict = vm.ctx.new_dict(); + dict.set_item("__method_def__", self.to_owned().into(), vm) + .unwrap(); + PyRef::new_ref( + function, + vm.ctx.types.method_descriptor_type.to_owned(), + Some(dict), + ) + } +} + +#[pyclass] +impl HeapMethodDef {} diff --git a/vm/src/function/mod.rs b/vm/src/function/mod.rs index 0521c06e4..409e4f1db 100644 --- a/vm/src/function/mod.rs +++ b/vm/src/function/mod.rs @@ -5,6 +5,7 @@ mod builtin; mod either; mod fspath; mod getset; +mod method; mod number; mod protocol; @@ -15,11 +16,12 @@ pub use argument::{ pub use arithmetic::{PyArithmeticValue, PyComparisonValue}; pub use buffer::{ArgAsciiBuffer, ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike}; pub(self) use builtin::{BorrowedParam, OwnedParam, RefParam}; -pub use builtin::{IntoPyNativeFunc, PyNativeFunc}; +pub use builtin::{IntoPyNativeFn, PyNativeFn}; pub use either::Either; pub use fspath::FsPath; pub use getset::PySetterValue; pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc}; +pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags}; pub use number::{ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgPrimitiveIndex, ArgSize}; pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence}; diff --git a/vm/src/macros.rs b/vm/src/macros.rs index e92959198..6e1953436 100644 --- a/vm/src/macros.rs +++ b/vm/src/macros.rs @@ -2,7 +2,7 @@ macro_rules! extend_module { ( $vm:expr, $module:expr, { $($name:expr => $value:expr),* $(,)? }) => {{ $( - $vm.__module_set_attr($module, $name, $value).unwrap(); + $vm.__module_set_attr($module, $vm.ctx.intern_str($name), $value).unwrap(); )* }}; } @@ -218,12 +218,13 @@ macro_rules! named_function { #[allow(unused_variables)] // weird lint, something to do with paste probably let ctx: &$crate::Context = &$ctx; $crate::__exports::paste::expr! { - ctx.make_func_def( - ctx.intern_str(stringify!($func)), + ctx.new_method_def( + stringify!($func), [<$module _ $func>], + ::rustpython_vm::function::PyMethodFlags::empty(), ) - .into_function() - .with_module(ctx.new_str(stringify!($module).to_owned()).into()) + .to_function() + .with_module(ctx.intern_str(stringify!($module)).into()) .into_ref(ctx) } }}; diff --git a/vm/src/object/core.rs b/vm/src/object/core.rs index 0ca1e248f..c5e42229d 100644 --- a/vm/src/object/core.rs +++ b/vm/src/object/core.rs @@ -551,7 +551,12 @@ impl PyObjectRef { /// T must be the exact payload type #[inline(always)] pub unsafe fn downcast_unchecked(self) -> PyRef { - PyRef::from_obj_unchecked(self) + // PyRef::from_obj_unchecked(self) + // manual impl to avoid assertion + let obj = ManuallyDrop::new(self); + PyRef { + ptr: obj.ptr.cast(), + } } /// # Safety diff --git a/vm/src/object/payload.rs b/vm/src/object/payload.rs index 0ea79c674..d5f3d6330 100644 --- a/vm/src/object/payload.rs +++ b/vm/src/object/payload.rs @@ -80,3 +80,7 @@ pub trait PyObjectPayload: } impl PyObjectPayload for T {} + +pub trait SlotOffset { + fn offset() -> usize; +} diff --git a/vm/src/protocol/callable.rs b/vm/src/protocol/callable.rs index eb404d021..ba20f2d32 100644 --- a/vm/src/protocol/callable.rs +++ b/vm/src/protocol/callable.rs @@ -16,6 +16,7 @@ impl PyObject { } /// PyObject_Call*Arg* series + #[inline] pub fn call(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult { let args = args.into_args(vm); self.call_with_args(args, vm) diff --git a/vm/src/stdlib/builtins.rs b/vm/src/stdlib/builtins.rs index a7522f097..e14a6847b 100644 --- a/vm/src/stdlib/builtins.rs +++ b/vm/src/stdlib/builtins.rs @@ -2,6 +2,7 @@ //! //! Implements the list of [builtin Python functions](https://docs.python.org/3/library/builtins.html). use crate::{builtins::PyModule, class::PyClassImpl, Py, VirtualMachine}; +pub(crate) use builtins::{__module_def, DOC}; pub use builtins::{ascii, print}; #[pymodule] diff --git a/vm/src/stdlib/sys.rs b/vm/src/stdlib/sys.rs index 780d7b4ef..97eae88db 100644 --- a/vm/src/stdlib/sys.rs +++ b/vm/src/stdlib/sys.rs @@ -1,6 +1,6 @@ use crate::{builtins::PyModule, convert::ToPyObject, Py, PyResult, VirtualMachine}; -pub(crate) use sys::{UnraisableHookArgs, MAXSIZE, MULTIARCH}; +pub(crate) use sys::{UnraisableHookArgs, __module_def, DOC, MAXSIZE, MULTIARCH}; #[pymodule] mod sys { diff --git a/vm/src/types/slot.rs b/vm/src/types/slot.rs index 4d2c273c5..001e6f972 100644 --- a/vm/src/types/slot.rs +++ b/vm/src/types/slot.rs @@ -3,7 +3,9 @@ use crate::{ bytecode::ComparisonOperator, common::hash::PyHash, convert::{ToPyObject, ToPyResult}, - function::{Either, FromArgs, FuncArgs, OptionalArg, PyComparisonValue, PySetterValue}, + function::{ + Either, FromArgs, FuncArgs, OptionalArg, PyComparisonValue, PyMethodDef, PySetterValue, + }, identifier, protocol::{ PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PyNumber, PyNumberMethods, @@ -62,6 +64,8 @@ pub struct PyTypeSlots { pub iter: AtomicCell>, pub iternext: AtomicCell>, + pub methods: &'static [PyMethodDef], + // Flags to define presence of optional/expanded features pub flags: PyTypeFlags, @@ -349,7 +353,7 @@ fn init_wrapper(obj: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResu Ok(()) } -fn new_wrapper(cls: PyTypeRef, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { +pub(crate) fn new_wrapper(cls: PyTypeRef, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { let new = cls.get_attr(identifier!(vm, __new__)).unwrap(); args.prepend_arg(cls.into()); new.call(args, vm) @@ -827,10 +831,15 @@ pub trait Callable: PyPayload { #[inline] #[pyslot] fn slot_call(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let Some(zelf) = zelf.downcast_ref() else { - let err = vm.new_downcast_type_error(Self::class(&vm.ctx), zelf); - return Err(err); - }; + let zelf = zelf.downcast_ref().ok_or_else(|| { + let repr = zelf.repr(vm); + let help = if let Ok(repr) = repr.as_ref() { + repr.as_str().to_owned() + } else { + zelf.class().name().to_owned() + }; + vm.new_type_error(format!("unexpected payload for __call__ of {help}")) + })?; let args = args.bind(vm)?; Self::call(zelf, args, vm) } diff --git a/vm/src/types/zoo.rs b/vm/src/types/zoo.rs index a2d2a5c79..492b584e2 100644 --- a/vm/src/types/zoo.rs +++ b/vm/src/types/zoo.rs @@ -74,6 +74,7 @@ pub struct TypeZoo { pub zip_type: &'static Py, pub function_type: &'static Py, pub builtin_function_or_method_type: &'static Py, + pub builtin_method_type: &'static Py, pub method_descriptor_type: &'static Py, pub property_type: &'static Py, pub getset_type: &'static Py, @@ -91,6 +92,9 @@ pub struct TypeZoo { pub generic_alias_type: &'static Py, pub union_type: &'static Py, pub member_descriptor_type: &'static Py, + + // RustPython-original types + pub method_def: &'static Py, } impl TypeZoo { @@ -135,7 +139,8 @@ impl TypeZoo { async_generator_wrapped_value: asyncgenerator::PyAsyncGenWrappedValue::init_builtin_type(), bound_method_type: function::PyBoundMethod::init_builtin_type(), - builtin_function_or_method_type: builtin_func::PyBuiltinFunction::init_builtin_type(), + builtin_function_or_method_type: builtin_func::PyNativeFunction::init_builtin_type(), + builtin_method_type: builtin_func::PyNativeMethod::init_builtin_type(), bytearray_iterator_type: bytearray::PyByteArrayIterator::init_builtin_type(), bytes_iterator_type: bytes::PyBytesIterator::init_builtin_type(), callable_iterator: iter::PyCallableIterator::init_builtin_type(), @@ -172,12 +177,14 @@ impl TypeZoo { traceback_type: traceback::PyTraceback::init_builtin_type(), tuple_iterator_type: tuple::PyTupleIterator::init_builtin_type(), weakproxy_type: weakproxy::PyWeakProxy::init_builtin_type(), - method_descriptor_type: builtin_func::PyBuiltinMethod::init_builtin_type(), + method_descriptor_type: descriptor::PyMethodDescriptor::init_builtin_type(), none_type: singletons::PyNone::init_builtin_type(), not_implemented_type: singletons::PyNotImplemented::init_builtin_type(), generic_alias_type: genericalias::PyGenericAlias::init_builtin_type(), union_type: union_::PyUnion::init_builtin_type(), member_descriptor_type: descriptor::PyMemberDescriptor::init_builtin_type(), + + method_def: crate::function::HeapMethodDef::init_builtin_type(), } } diff --git a/vm/src/vm/context.rs b/vm/src/vm/context.rs index c50f2a83e..f9982c9e9 100644 --- a/vm/src/vm/context.rs +++ b/vm/src/vm/context.rs @@ -1,11 +1,10 @@ use crate::{ builtins::{ - builtin_func::{PyBuiltinFunction, PyBuiltinMethod, PyNativeFuncDef}, bytes, code::{self, PyCode}, descriptor::{ - DescrObject, MemberGetter, MemberKind, MemberSetter, MemberSetterFunc, PyMemberDef, - PyMemberDescriptor, + MemberGetter, MemberKind, MemberSetter, MemberSetterFunc, PyDescriptorOwned, + PyMemberDef, PyMemberDescriptor, }, getset::PyGetSet, object, pystr, @@ -17,7 +16,10 @@ use crate::{ class::{PyClassImpl, StaticType}, common::rc::PyRc, exceptions, - function::{IntoPyGetterFunc, IntoPyNativeFunc, IntoPySetterFunc}, + function::{ + HeapMethodDef, IntoPyGetterFunc, IntoPyNativeFn, IntoPySetterFunc, PyMethodDef, + PyMethodFlags, + }, intern::{InternableString, MaybeInternedString, StringPool}, object::{Py, PyObjectPayload, PyObjectRef, PyPayload, PyRef}, types::{PyTypeFlags, PyTypeSlots, TypeZoo}, @@ -45,7 +47,7 @@ pub struct Context { pub int_cache_pool: Vec, // there should only be exact objects of str in here, no non-str objects and no subclasses pub(crate) string_pool: StringPool, - pub(crate) slot_new_wrapper: PyRef, + pub(crate) slot_new_wrapper: PyMethodDef, pub names: ConstName, } @@ -287,14 +289,16 @@ impl Context { let string_pool = StringPool::default(); let names = unsafe { ConstName::new(&string_pool, &types.str_type.to_owned()) }; - let slot_new_wrapper = create_object( - PyNativeFuncDef::new(PyType::__new__.into_func(), names.__new__).into_function(), - types.builtin_function_or_method_type, - ); + let slot_new_wrapper = PyMethodDef { + name: names.__new__.as_str(), + func: PyType::__new__.into_func(), + flags: PyMethodFlags::METHOD, + doc: None, + }; let empty_str = unsafe { string_pool.intern("", types.str_type.to_owned()) }; let empty_bytes = create_object(PyBytes::from(Vec::new()), types.bytes_type); - let context = Context { + Context { true_value, false_value, none, @@ -312,10 +316,7 @@ impl Context { string_pool, slot_new_wrapper, names, - }; - TypeZoo::extend(&context); - exceptions::ExceptionZoo::extend(&context); - context + } } pub fn intern_str(&self, s: S) -> &'static PyStrInterned { @@ -484,12 +485,24 @@ impl Context { .unwrap() } - #[inline] - pub fn make_func_def(&self, name: &'static PyStrInterned, f: F) -> PyNativeFuncDef + pub fn new_method_def( + &self, + name: &'static str, + f: F, + flags: PyMethodFlags, + doc: Option<&'static str>, + ) -> PyRef where - F: IntoPyNativeFunc, + F: IntoPyNativeFn, { - PyNativeFuncDef::new(f.into_func(), name) + let def = PyMethodDef { + name, + func: f.into_func(), + flags, + doc, + }; + let payload = HeapMethodDef::new(def); + PyRef::new_ref(payload, self.types.method_def.to_owned(), None) } #[inline] @@ -509,40 +522,14 @@ impl Context { doc: None, }; let member_descriptor = PyMemberDescriptor { - common: DescrObject { + common: PyDescriptorOwned { typ: class.to_owned(), name: self.intern_str(name), qualname: PyRwLock::new(None), }, member: member_def, }; - - PyRef::new_ref( - member_descriptor, - self.types.member_descriptor_type.to_owned(), - None, - ) - } - - // #[deprecated] - pub fn new_function(&self, name: &str, f: F) -> PyRef - where - F: IntoPyNativeFunc, - { - self.make_func_def(self.intern_str(name), f) - .build_function(self) - } - - pub fn new_method( - &self, - name: &'static PyStrInterned, - class: &'static Py, - f: F, - ) -> PyRef - where - F: IntoPyNativeFunc, - { - PyBuiltinMethod::new_ref(name, class, f, self) + member_descriptor.into_ref(self) } pub fn new_readonly_getset( diff --git a/vm/src/vm/interpreter.rs b/vm/src/vm/interpreter.rs index 1a1a6cc97..de928636a 100644 --- a/vm/src/vm/interpreter.rs +++ b/vm/src/vm/interpreter.rs @@ -46,6 +46,8 @@ impl Interpreter { F: FnOnce(&mut VirtualMachine), { let ctx = Context::genesis(); + crate::types::TypeZoo::extend(ctx); + crate::exceptions::ExceptionZoo::extend(ctx); let mut vm = VirtualMachine::new(settings, ctx.clone()); init(&mut vm); vm.initialize(); diff --git a/vm/src/vm/mod.rs b/vm/src/vm/mod.rs index 2e546d3d6..d993cdb93 100644 --- a/vm/src/vm/mod.rs +++ b/vm/src/vm/mod.rs @@ -116,17 +116,17 @@ impl VirtualMachine { // make a new module without access to the vm; doesn't // set __spec__, __loader__, etc. attributes - let new_module = || { + let new_module = |def| { PyRef::new_ref( - PyModule::new(), + PyModule::from_def(def), ctx.types.module_type.to_owned(), Some(ctx.new_dict()), ) }; // Hard-core modules: - let builtins = new_module(); - let sys_module = new_module(); + let builtins = new_module(stdlib::builtins::__module_def(&ctx)); + let sys_module = new_module(stdlib::sys::__module_def(&ctx)); let import_func = ctx.none(); let profile_func = RefCell::new(ctx.none()); @@ -199,11 +199,17 @@ impl VirtualMachine { let frozen = frozen::core_frozen_inits().collect(); PyRc::get_mut(&mut vm.state).unwrap().frozen = frozen; - vm.builtins - .init_module_dict(vm.ctx.intern_str("builtins"), None, &vm); - vm.sys_module - .init_module_dict(vm.ctx.intern_str("sys"), None, &vm); - + vm.builtins.init_dict( + vm.ctx.intern_str("builtins"), + Some(vm.ctx.intern_str(stdlib::builtins::DOC.unwrap()).to_owned()), + &vm, + ); + vm.sys_module.init_dict( + vm.ctx.intern_str("sys"), + Some(vm.ctx.intern_str(stdlib::sys::DOC.unwrap()).to_owned()), + &vm, + ); + // let name = vm.sys_module.get_attr("__name__", &vm).unwrap(); vm } @@ -251,7 +257,7 @@ impl VirtualMachine { self.state_mut().settings.path_list.insert(0, "".to_owned()); stdlib::builtins::init_module(self, &self.builtins); - stdlib::sys::init_module(self, self.sys_module.as_ref(), self.builtins.as_ref()); + stdlib::sys::init_module(self, &self.sys_module, &self.builtins); let mut essential_init = || -> PyResult { #[cfg(not(target_arch = "wasm32"))] @@ -802,15 +808,13 @@ impl VirtualMachine { pub fn __module_set_attr( &self, module: &Py, - attr_name: &str, + attr_name: &'static PyStrInterned, attr_value: impl Into, ) -> PyResult<()> { let val = attr_value.into(); - module.as_object().generic_setattr( - self.ctx.intern_str(attr_name), - PySetterValue::Assign(val), - self, - ) + module + .as_object() + .generic_setattr(attr_name, PySetterValue::Assign(val), self) } pub fn insert_sys_path(&self, obj: PyObjectRef) -> PyResult<()> { diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index 38b37f81f..301c488f0 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -1,9 +1,12 @@ use crate::{ builtins::{ + builtin_func::PyNativeFunction, + descriptor::PyMethodDescriptor, tuple::{IntoPyTuple, PyTupleRef}, PyBaseException, PyBaseExceptionRef, PyDictRef, PyModule, PyStrRef, PyType, PyTypeRef, }, convert::ToPyObject, + function::{IntoPyNativeFn, PyMethodFlags}, scope::Scope, vm::VirtualMachine, AsObject, Py, PyObject, PyObjectRef, PyRef, @@ -20,17 +23,18 @@ impl VirtualMachine { value.into_pytuple(self) } - pub fn new_module(&self, name: &str, dict: PyDictRef, doc: Option<&str>) -> PyRef { + pub fn new_module( + &self, + name: &str, + dict: PyDictRef, + doc: Option, + ) -> PyRef { let module = PyRef::new_ref( PyModule::new(), self.ctx.types.module_type.to_owned(), Some(dict), ); - module.init_module_dict( - self.ctx.intern_str(name), - doc.map(|doc| self.ctx.new_str(doc)), - self, - ); + module.init_dict(self.ctx.intern_str(name), doc, self); module } @@ -38,6 +42,31 @@ impl VirtualMachine { Scope::with_builtins(None, self.ctx.new_dict(), self) } + pub fn new_function(&self, name: &'static str, f: F) -> PyRef + where + F: IntoPyNativeFn, + { + let def = self + .ctx + .new_method_def(name, f, PyMethodFlags::empty(), None); + def.build_function(self) + } + + pub fn new_method( + &self, + name: &'static str, + class: &'static Py, + f: F, + ) -> PyRef + where + F: IntoPyNativeFn, + { + let def = self + .ctx + .new_method_def(name, f, PyMethodFlags::METHOD, None); + def.build_method(class, self) + } + /// Instantiate an exception with arguments. /// This function should only be used with builtin exception types; if a user-defined exception /// type is passed in, it may not be fully initialized; try using diff --git a/wasm/lib/src/convert.rs b/wasm/lib/src/convert.rs index f91f153f2..ab2dc6da8 100644 --- a/wasm/lib/src/convert.rs +++ b/wasm/lib/src/convert.rs @@ -214,26 +214,25 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef { } } else if js_val.is_function() { let func = js_sys::Function::from(js_val); - vm.ctx - .new_function( - String::from(func.name()).as_str(), - move |args: FuncArgs, vm: &VirtualMachine| -> PyResult { - let this = Object::new(); - for (k, v) in args.kwargs { - Reflect::set(&this, &k.into(), &py_to_js(vm, v)) - .expect("property to be settable"); - } - let js_args = args - .args - .into_iter() - .map(|v| py_to_js(vm, v)) - .collect::(); - func.apply(&this, &js_args) - .map(|val| js_to_py(vm, val)) - .map_err(|err| js_err_to_py_err(vm, &err)) - }, - ) - .into() + vm.new_function( + vm.ctx.intern_str(String::from(func.name())).as_str(), + move |args: FuncArgs, vm: &VirtualMachine| -> PyResult { + let this = Object::new(); + for (k, v) in args.kwargs { + Reflect::set(&this, &k.into(), &py_to_js(vm, v)) + .expect("property to be settable"); + } + let js_args = args + .args + .into_iter() + .map(|v| py_to_js(vm, v)) + .collect::(); + func.apply(&this, &js_args) + .map(|val| js_to_py(vm, val)) + .map_err(|err| js_err_to_py_err(vm, &err)) + }, + ) + .into() } else if let Some(err) = js_val.dyn_ref::() { js_err_to_py_err(vm, err).into() } else if js_val.is_undefined() { diff --git a/wasm/lib/src/js_module.rs b/wasm/lib/src/js_module.rs index 7cdfd9d39..f0c5378c3 100644 --- a/wasm/lib/src/js_module.rs +++ b/wasm/lib/src/js_module.rs @@ -423,8 +423,8 @@ mod _js { }; let _ = then.call( ( - vm.ctx.new_function("resolve", resolve), - vm.ctx.new_function("reject", reject), + vm.new_function("resolve", resolve), + vm.new_function("reject", reject), ), vm, ); diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs index 25964efc2..0c62bb32e 100644 --- a/wasm/lib/src/vm_class.rs +++ b/wasm/lib/src/vm_class.rs @@ -316,7 +316,7 @@ impl WASMVirtualMachine { let (key, value) = entry?; let key = Object::from(key).to_string(); extend_module!(vm, &py_module, { - &String::from(key) => convert::js_to_py(vm, value), + String::from(key) => convert::js_to_py(vm, value), }); } diff --git a/wasm/lib/src/wasm_builtins.rs b/wasm/lib/src/wasm_builtins.rs index 216360c1c..59d7880af 100644 --- a/wasm/lib/src/wasm_builtins.rs +++ b/wasm/lib/src/wasm_builtins.rs @@ -29,14 +29,14 @@ pub fn make_stdout_object( vm.ctx.types.object_type.to_owned(), {} )); - let write_method = ctx.new_method( - ctx.intern_str("write"), + let write_method = vm.new_method( + "write", cls, move |_self: PyObjectRef, data: PyStrRef, vm: &VirtualMachine| -> PyResult<()> { write_f(data.as_str(), vm) }, ); - let flush_method = ctx.new_method(ctx.intern_str("flush"), cls, |_self: PyObjectRef| {}); + let flush_method = vm.new_method("flush", cls, |_self: PyObjectRef| {}); extend_class!(ctx, cls, { "write" => write_method, "flush" => flush_method,