Infer native calling convention flags from argument metadata (#9005)

`PyMethodDef::new_const`, `PyMethodDef::new_raw_const` and
`Context::new_method_def` add the calling convention (`NOARGS`, `O`,
`FASTCALL`, or `FASTCALL | KEYWORDS`) inferred from the
`FromArgs::PARAMS` of each argument, exposed as `IntoPyNativeFn::ARGS`.
Flags that already contain a calling convention, such as C-API
`ml_flags`, are kept. For `METHOD` and `CLASS` the first argument is the
bound receiver. Raw functions get `FASTCALL | KEYWORDS`.

The derive macros pass only binding flags and no longer infer call flags
from Rust type names. Definitions built without the macros, such as the
`__new__` wrapper and `vm.new_function`, get calling convention flags the
same way.

Assisted-by: Claude Code:claude-opus-5-5
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-10-08 17:43:30 +09:00
1 parent ca9dbb40f8
commit bb9ef5d0c2
7 files changed
+180 -96

No files matched your search

+3 -9
View File
@@ -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
+3 -7
View File
@@ -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<TokenStream>,
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(())
}
-72
View File
@@ -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 },
}
}
}
+24 -1
View File
@@ -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<F> PyNativeFn for F where
/// just pass an unconstrained generic type, e.g.
/// `fn foo<F, FKind>(f: F) where F: IntoPyNativeFn<FKind>`
pub trait IntoPyNativeFn<Kind>: 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::<FuncArgs>("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<F, T, R, VM> IntoPyNativeFn<(T, R, VM)> for F
where
F: PyNativeFnInternal<T, R, VM>,
{
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<T, R, VM>: 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<S>, $($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<S>, $($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<S>, $($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<S>, $($T,)*)>(vm, callee)?;
+21 -5
View File
@@ -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<Kind>(
pub const fn new_const<Kind, F: IntoPyNativeFn<Kind>>(
name: &'static str,
func: impl IntoPyNativeFn<Kind>,
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::<FuncArgs>("args")]),
#[cfg(feature = "doc")]
doc_off: doc.offset,
#[cfg(feature = "doc")]
+128 -1
View File
@@ -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(&params[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), "<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);
}
}
+1 -1
View File
@@ -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")]