diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index be59d7a33..b93542059 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -1,8 +1,8 @@ use super::Diagnostic; use crate::util::{ ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta, - ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, infer_native_call_flags, - internal_doc_tokens, pyclass_ident_and_attrs, pyexception_ident_and_attrs, + ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, internal_doc_tokens, + pyclass_ident_and_attrs, pyexception_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree}; @@ -1307,9 +1307,6 @@ where _ => None, } }; - let drop_first_typed = usize::from(implicit_self.is_some()); - let call_flags = infer_native_call_flags(func.sig(), drop_first_typed); - // Add #[allow(non_snake_case)] for setter methods like set___name__ let method_name = ident.to_string(); if method_name.starts_with("set_") && method_name.contains("__") { @@ -1341,7 +1338,6 @@ where raw, coexist, attr_name: self.inner.attr_name, - call_flags, }); Ok(()) } @@ -1538,7 +1534,6 @@ struct MethodNurseryItem { doc: TokenStream, doc_body_pending: TokenStream, attr_name: AttrName, - call_flags: TokenStream, } impl MethodNursery { @@ -1578,7 +1573,6 @@ impl ToTokens for MethodNursery { } _ => unreachable!(), }; - let call_flags = &item.call_flags; let coexist_flags = if item.coexist { quote! { | rustpython_vm::function::PyMethodFlags::COEXIST.bits() } } else { @@ -1586,7 +1580,7 @@ impl ToTokens for MethodNursery { }; let flags = quote! { rustpython_vm::function::PyMethodFlags::from_bits_retain( - (#binding_flags).bits() | (#call_flags).bits() #coexist_flags + (#binding_flags).bits() #coexist_flags ) }; // TODO: intern diff --git a/crates/derive-impl/src/pymodule.rs b/crates/derive-impl/src/pymodule.rs index 14ad5ea3c..9a2ab6878 100644 --- a/crates/derive-impl/src/pymodule.rs +++ b/crates/derive-impl/src/pymodule.rs @@ -2,8 +2,8 @@ use crate::error::Diagnostic; use crate::pystructseq::PyStructSequenceMeta; use crate::util::{ ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, - ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, infer_native_call_flags, - internal_doc_tokens, iter_use_idents, pyclass_ident_and_attrs, + ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, internal_doc_tokens, + iter_use_idents, pyclass_ident_and_attrs, }; use core::str::FromStr; use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; @@ -536,7 +536,6 @@ struct FunctionNurseryItem { ident: Ident, /// One internal doc per [`py_names`](Self::py_names) entry. docs: Vec, - call_flags: TokenStream, } impl FunctionNursery { @@ -566,14 +565,13 @@ impl ToTokens for ValidatedFunctionNursery { let ident = &item.ident; let cfgs = &item.cfgs; let cfgs = quote!(#(#cfgs)*); - let flags = &item.call_flags; for (py_name, doc) in item.py_names.iter().zip(&item.docs) { inner_tokens.extend(quote![ #cfgs rustpython_vm::function::PyMethodDef::new_const( #py_name, #ident, - #flags, + rustpython_vm::function::PyMethodFlags::empty(), #doc, ), ]); @@ -711,14 +709,12 @@ impl ModuleItem for FunctionItem { internal_doc_tokens(func.sig(), py_name, None, doc, None, Some("$module")) }) .collect(); - let call_flags = infer_native_call_flags(func.sig(), 0); args.context.function_items.add_item(FunctionNurseryItem { ident: ident.to_owned(), py_names, cfgs: args.cfgs.to_vec(), docs, - call_flags, }); Ok(()) } diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 985a83123..6a14dd116 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -936,75 +936,3 @@ pub(crate) fn internal_doc_tokens( } } } - -pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize) -> TokenStream { - // Best-effort mapping of Rust function signatures to CPython-style - // METH_* calling convention flags used by CALL specialization. - let mut typed_args = Vec::new(); - for arg in &sig.inputs { - let FnArg::Typed(typed) = arg else { - continue; - }; - let ty_tokens = &typed.ty; - let ty = quote!(#ty_tokens).to_string().replace(' ', ""); - // The interpreter supplies `vm` and `callee`; a Python call never - // passes them. - if (ty.starts_with('&') && ty.ends_with("VirtualMachine")) || ty.ends_with("Callee") { - continue; - } - typed_args.push(ty); - } - - let mut user_args = typed_args.into_iter(); - for _ in 0..drop_first_typed { - if user_args.next().is_none() { - break; - } - } - - let mut has_keywords = false; - let mut variable_arity = false; - let mut fixed_positional = 0usize; - - for ty in user_args { - let is_named = |name: &str| { - ty == name - || ty.starts_with(&format!("{name}<")) - || ty.contains(&format!("::{name}<")) - || ty.ends_with(&format!("::{name}")) - }; - - if is_named("FuncArgs") { - has_keywords = true; - variable_arity = true; - continue; - } - if is_named("KwArgs") { - has_keywords = true; - variable_arity = true; - continue; - } - if is_named("PosArgs") || is_named("OptionalArg") || is_named("OptionalOption") { - variable_arity = true; - continue; - } - fixed_positional += 1; - } - - if has_keywords { - quote! { - rustpython_vm::function::PyMethodFlags::from_bits_retain( - rustpython_vm::function::PyMethodFlags::FASTCALL.bits() - | rustpython_vm::function::PyMethodFlags::KEYWORDS.bits() - ) - } - } else if variable_arity { - quote! { rustpython_vm::function::PyMethodFlags::FASTCALL } - } else { - match fixed_positional { - 0 => quote! { rustpython_vm::function::PyMethodFlags::NOARGS }, - 1 => quote! { rustpython_vm::function::PyMethodFlags::O }, - _ => quote! { rustpython_vm::function::PyMethodFlags::FASTCALL }, - } - } -} diff --git a/crates/vm/src/function/builtin.rs b/crates/vm/src/function/builtin.rs index b4bd61077..bda24f879 100644 --- a/crates/vm/src/function/builtin.rs +++ b/crates/vm/src/function/builtin.rs @@ -1,4 +1,4 @@ -use super::{Callee, FromArgs, FuncArgs}; +use super::{Callee, FromArgs, FuncArgs, SigArg}; use crate::{ Py, PyPayload, PyRef, PyResult, VirtualMachine, convert::ToPyResult, object::PyThreadingConstraint, @@ -37,6 +37,10 @@ impl PyNativeFn for F where /// just pass an unconstrained generic type, e.g. /// `fn foo(f: F) where F: IntoPyNativeFn` pub trait IntoPyNativeFn: Sized + PyThreadingConstraint + 'static { + /// Binding metadata of each argument, from which the calling convention + /// is inferred. A `&self` receiver is the `$self` marker. + const ARGS: &'static [SigArg] = &[SigArg::from_arg::("args")]; + fn call(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult; /// `IntoPyNativeFn::into_func()` generates a PyNativeFn that performs the @@ -89,6 +93,8 @@ impl IntoPyNativeFn<(T, R, VM)> for F where F: PyNativeFnInternal, { + const ARGS: &'static [SigArg] = F::ARGS; + #[inline(always)] fn call(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { self.call_(vm, args, callee) @@ -98,6 +104,7 @@ where mod sealed { use super::*; pub trait PyNativeFnInternal: Sized + PyThreadingConstraint + 'static { + const ARGS: &'static [SigArg]; fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult; } } @@ -124,6 +131,8 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = &[$(SigArg::from_arg::<$T>("")),*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let ($($n,)*) = args.bind_for::<($($T,)*)>(vm, callee)?; @@ -138,6 +147,9 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = + &[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let (zelf, $($n,)*) = args.bind_for::<(PyRef, $($T,)*)>(vm, callee)?; @@ -152,6 +164,9 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = + &[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let (zelf, $($n,)*) = args.bind_for::<(PyRef, $($T,)*)>(vm, callee)?; @@ -165,6 +180,8 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = &[$(SigArg::from_arg::<$T>("")),*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let ($($n,)*) = args.bind_for::<($($T,)*)>(vm, callee)?; @@ -179,6 +196,9 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = + &[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let (zelf, $($n,)*) = args.bind_for::<(PyRef, $($T,)*)>(vm, callee)?; @@ -193,6 +213,9 @@ macro_rules! into_py_native_fn_tuple { $($T: FromArgs,)* R: ToPyResult, { + const ARGS: &'static [SigArg] = + &[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*]; + fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult { let (zelf, $($n,)*) = args.bind_for::<(PyRef, $($T,)*)>(vm, callee)?; diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index c9953f8a7..b33da2877 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -6,7 +6,7 @@ use crate::{ descriptor::{PyClassMethodDescriptor, PyMethodDescriptor}, }, class::PyClassDef, - function::{IntoPyNativeFn, PyNativeFn}, + function::{FuncArgs, IntoPyNativeFn, PyNativeFn, SigArg}, }; bitflags::bitflags! { @@ -49,6 +49,22 @@ bitflags::bitflags! { impl PyMethodFlags { // FIXME: macro temp pub const EMPTY: Self = Self::empty(); + + const CALL_CONVENTION: Self = Self::VARARGS + .union(Self::KEYWORDS) + .union(Self::NOARGS) + .union(Self::O) + .union(Self::FASTCALL); + + /// Adds the calling convention inferred from `args` unless one is set. + /// For METHOD and CLASS the first argument is the bound receiver. + pub(crate) const fn with_call_convention(self, args: &[SigArg]) -> Self { + if self.intersects(Self::CALL_CONVENTION) { + return self; + } + let receiver = self.intersects(Self::METHOD.union(Self::CLASS)); + self.union(super::signature::native_call_flags(args, receiver)) + } } #[macro_export] @@ -115,16 +131,16 @@ impl PyMethodDef { } #[inline] - pub const fn new_const( + pub const fn new_const>( name: &'static str, - func: impl IntoPyNativeFn, + func: F, flags: PyMethodFlags, doc: super::ItemDoc, ) -> Self { Self { name, func: super::static_func(func), - flags, + flags: flags.with_call_convention(F::ARGS), #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] @@ -145,7 +161,7 @@ impl PyMethodDef { Self { name, func: super::static_raw_func(func), - flags, + flags: flags.with_call_convention(&[SigArg::from_arg::("args")]), #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")] diff --git a/crates/vm/src/function/signature.rs b/crates/vm/src/function/signature.rs index b43595ee3..74cf058d0 100644 --- a/crates/vm/src/function/signature.rs +++ b/crates/vm/src/function/signature.rs @@ -135,6 +135,81 @@ impl SigArg { } } +/// Calling-convention bits from each argument's binding metadata. +/// +/// Inspect arguments separately: a tuple's `PARAMS` can erase leaf metadata. +/// With `receiver`, the first argument is the bound `$self` / `$type`; it +/// contributes only the parameters its type supplies. +/// A keyword-capable parameter uses `FASTCALL | KEYWORDS`. +#[must_use] +pub(crate) const fn native_call_flags(args: &[SigArg], receiver: bool) -> super::PyMethodFlags { + let mut has_keywords = false; + let mut variable = false; + let mut fixed = 0usize; + let mut i = 0; + while i < args.len() { + match args[i].params { + Some(params) => count_params(params, &mut has_keywords, &mut variable, &mut fixed), + None if receiver && i == 0 => {} + None => fixed += 1, + } + i += 1; + } + if has_keywords { + super::PyMethodFlags::FASTCALL.union(super::PyMethodFlags::KEYWORDS) + } else if variable { + super::PyMethodFlags::FASTCALL + } else { + match fixed { + 0 => super::PyMethodFlags::NOARGS, + 1 => super::PyMethodFlags::O, + _ => super::PyMethodFlags::FASTCALL, + } + } +} + +const fn count_params( + params: &[Param], + has_keywords: &mut bool, + variable: &mut bool, + fixed: &mut usize, +) { + let mut i = 0; + while i < params.len() { + count_param(¶ms[i], has_keywords, variable, fixed); + i += 1; + } +} + +const fn count_param( + param: &Param, + has_keywords: &mut bool, + variable: &mut bool, + fixed: &mut usize, +) { + match param.kind { + ParamKind::PositionalOrKeyword => { + *has_keywords = true; + if param.default.is_some() { + *variable = true; + } else { + *fixed += 1; + } + } + ParamKind::KeywordOnly | ParamKind::VarKeyword => *has_keywords = true, + ParamKind::VarPositional => *variable = true, + ParamKind::PositionalOnly => { + if param.default.is_some() { + *variable = true; + } else { + *fixed += 1; + } + } + ParamKind::Flatten(Some(inner)) => count_params(inner, has_keywords, variable, fixed), + ParamKind::Flatten(None) => {} + } +} + const fn name_eq(name: &str, bytes: &[u8]) -> bool { let got = name.as_bytes(); if got.len() != bytes.len() { @@ -631,7 +706,8 @@ const fn write_args(buf: &mut [u8], mut st: St, args: &[SigArg]) -> St { #[cfg(test)] mod tests { - use super::{DefaultRepr, Param, ParamKind, St, write_one}; + use super::{DefaultRepr, Param, ParamKind, SigArg, St, native_call_flags, write_one}; + use crate::function::PyMethodFlags; fn rendered(default: DefaultRepr) -> String { let mut buf = [0u8; 64]; @@ -674,4 +750,55 @@ mod tests { assert_eq!(rendered(DefaultRepr::Raw("sys.maxsize")), "sys.maxsize"); assert_eq!(rendered(DefaultRepr::Unrepresentable), ""); } + + #[test] + fn native_call_flags_from_params() { + const fn arg(params: Option<&'static [Param]>) -> SigArg { + SigArg { name: "", params } + } + let keywords = PyMethodFlags::FASTCALL.union(PyMethodFlags::KEYWORDS); + assert_eq!(native_call_flags(&[], false), PyMethodFlags::NOARGS); + assert_eq!(native_call_flags(&[arg(None)], false), PyMethodFlags::O); + assert_eq!( + native_call_flags(&[arg(None), arg(None)], false), + PyMethodFlags::FASTCALL + ); + // The receiver is not a Python argument. + assert_eq!( + native_call_flags(&[SigArg::marker("$self"), arg(None)], true), + PyMethodFlags::O + ); + assert_eq!(native_call_flags(&[arg(None)], true), PyMethodFlags::NOARGS); + + const OPTIONAL: &[Param] = &[Param { + name: "", + kind: ParamKind::PositionalOnly, + default: Some(DefaultRepr::Unrepresentable), + }]; + assert_eq!( + native_call_flags(&[arg(Some(OPTIONAL))], false), + PyMethodFlags::FASTCALL + ); + + const KW: &[Param] = &[Param::positional_or_keyword("exp")]; + assert_eq!(native_call_flags(&[arg(Some(KW))], false), keywords); + + const FUNCARGS: &[Param] = &[Param::var_positional("args"), Param::var_keyword("kwargs")]; + assert_eq!(native_call_flags(&[arg(Some(FUNCARGS))], false), keywords); + // A receiver that binds the whole call keeps its parameters. + assert_eq!(native_call_flags(&[arg(Some(FUNCARGS))], true), keywords); + + const POSARGS: &[Param] = &[Param::var_positional("args")]; + assert_eq!( + native_call_flags(&[arg(Some(POSARGS))], false), + PyMethodFlags::FASTCALL + ); + + const INNER: &[Param] = &[Param::keyword_only( + "reverse", + Some(DefaultRepr::Bool(false)), + )]; + const FLAT: &[Param] = &[Param::flatten(Some(INNER))]; + assert_eq!(native_call_flags(&[arg(Some(FLAT))], false), keywords); + } } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 78e6bc6fe..42da0551c 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -662,7 +662,7 @@ impl Context { let def = PyMethodDef { name, func: Box::leak(Box::new(f.into_func())), - flags, + flags: flags.with_call_convention(F::ARGS), #[cfg(feature = "doc")] doc_off: doc.offset, #[cfg(feature = "doc")]