Visitar URL original
Infer native calling convention flags from argument metadata (#9005) · RustPython/RustPython@bb9ef5d · GitHub
Skip to content

Commit bb9ef5d

Browse files
authored
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
1 parent ca9dbb4 commit bb9ef5d

7 files changed

Lines changed: 180 additions & 96 deletions

File tree

‎crates/derive-impl/src/pyclass.rs‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::Diagnostic;
22
use crate::util::{
33
ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta,
4-
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, infer_native_call_flags,
5-
internal_doc_tokens, pyclass_ident_and_attrs, pyexception_ident_and_attrs,
4+
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, internal_doc_tokens,
5+
pyclass_ident_and_attrs, pyexception_ident_and_attrs,
66
};
77
use core::str::FromStr;
88
use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
@@ -1307,9 +1307,6 @@ where
13071307
_ => None,
13081308
}
13091309
};
1310-
let drop_first_typed = usize::from(implicit_self.is_some());
1311-
let call_flags = infer_native_call_flags(func.sig(), drop_first_typed);
1312-
13131310
// Add #[allow(non_snake_case)] for setter methods like set___name__
13141311
let method_name = ident.to_string();
13151312
if method_name.starts_with("set_") && method_name.contains("__") {
@@ -1341,7 +1338,6 @@ where
13411338
raw,
13421339
coexist,
13431340
attr_name: self.inner.attr_name,
1344-
call_flags,
13451341
});
13461342
Ok(())
13471343
}
@@ -1538,7 +1534,6 @@ struct MethodNurseryItem {
15381534
doc: TokenStream,
15391535
doc_body_pending: TokenStream,
15401536
attr_name: AttrName,
1541-
call_flags: TokenStream,
15421537
}
15431538

15441539
impl MethodNursery {
@@ -1578,15 +1573,14 @@ impl ToTokens for MethodNursery {
15781573
}
15791574
_ => unreachable!(),
15801575
};
1581-
let call_flags = &item.call_flags;
15821576
let coexist_flags = if item.coexist {
15831577
quote! { | rustpython_vm::function::PyMethodFlags::COEXIST.bits() }
15841578
} else {
15851579
quote! {}
15861580
};
15871581
let flags = quote! {
15881582
rustpython_vm::function::PyMethodFlags::from_bits_retain(
1589-
(#binding_flags).bits() | (#call_flags).bits() #coexist_flags
1583+
(#binding_flags).bits() #coexist_flags
15901584
)
15911585
};
15921586
// TODO: intern

‎crates/derive-impl/src/pymodule.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::error::Diagnostic;
22
use crate::pystructseq::PyStructSequenceMeta;
33
use crate::util::{
44
ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner,
5-
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, infer_native_call_flags,
6-
internal_doc_tokens, iter_use_idents, pyclass_ident_and_attrs,
5+
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, internal_doc_tokens,
6+
iter_use_idents, pyclass_ident_and_attrs,
77
};
88
use core::str::FromStr;
99
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
@@ -536,7 +536,6 @@ struct FunctionNurseryItem {
536536
ident: Ident,
537537
/// One internal doc per [`py_names`](Self::py_names) entry.
538538
docs: Vec<TokenStream>,
539-
call_flags: TokenStream,
540539
}
541540

542541
impl FunctionNursery {
@@ -566,14 +565,13 @@ impl ToTokens for ValidatedFunctionNursery {
566565
let ident = &item.ident;
567566
let cfgs = &item.cfgs;
568567
let cfgs = quote!(#(#cfgs)*);
569-
let flags = &item.call_flags;
570568
for (py_name, doc) in item.py_names.iter().zip(&item.docs) {
571569
inner_tokens.extend(quote![
572570
#cfgs
573571
rustpython_vm::function::PyMethodDef::new_const(
574572
#py_name,
575573
#ident,
576-
#flags,
574+
rustpython_vm::function::PyMethodFlags::empty(),
577575
#doc,
578576
),
579577
]);
@@ -711,14 +709,12 @@ impl ModuleItem for FunctionItem {
711709
internal_doc_tokens(func.sig(), py_name, None, doc, None, Some("$module"))
712710
})
713711
.collect();
714-
let call_flags = infer_native_call_flags(func.sig(), 0);
715712

716713
args.context.function_items.add_item(FunctionNurseryItem {
717714
ident: ident.to_owned(),
718715
py_names,
719716
cfgs: args.cfgs.to_vec(),
720717
docs,
721-
call_flags,
722718
});
723719
Ok(())
724720
}

‎crates/derive-impl/src/util.rs‎

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -936,75 +936,3 @@ pub(crate) fn internal_doc_tokens(
936936
}
937937
}
938938
}
939-
940-
pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize) -> TokenStream {
941-
// Best-effort mapping of Rust function signatures to CPython-style
942-
// METH_* calling convention flags used by CALL specialization.
943-
let mut typed_args = Vec::new();
944-
for arg in &sig.inputs {
945-
let FnArg::Typed(typed) = arg else {
946-
continue;
947-
};
948-
let ty_tokens = &typed.ty;
949-
let ty = quote!(#ty_tokens).to_string().replace(' ', "");
950-
// The interpreter supplies `vm` and `callee`; a Python call never
951-
// passes them.
952-
if (ty.starts_with('&') && ty.ends_with("VirtualMachine")) || ty.ends_with("Callee") {
953-
continue;
954-
}
955-
typed_args.push(ty);
956-
}
957-
958-
let mut user_args = typed_args.into_iter();
959-
for _ in 0..drop_first_typed {
960-
if user_args.next().is_none() {
961-
break;
962-
}
963-
}
964-
965-
let mut has_keywords = false;
966-
let mut variable_arity = false;
967-
let mut fixed_positional = 0usize;
968-
969-
for ty in user_args {
970-
let is_named = |name: &str| {
971-
ty == name
972-
|| ty.starts_with(&format!("{name}<"))
973-
|| ty.contains(&format!("::{name}<"))
974-
|| ty.ends_with(&format!("::{name}"))
975-
};
976-
977-
if is_named("FuncArgs") {
978-
has_keywords = true;
979-
variable_arity = true;
980-
continue;
981-
}
982-
if is_named("KwArgs") {
983-
has_keywords = true;
984-
variable_arity = true;
985-
continue;
986-
}
987-
if is_named("PosArgs") || is_named("OptionalArg") || is_named("OptionalOption") {
988-
variable_arity = true;
989-
continue;
990-
}
991-
fixed_positional += 1;
992-
}
993-
994-
if has_keywords {
995-
quote! {
996-
rustpython_vm::function::PyMethodFlags::from_bits_retain(
997-
rustpython_vm::function::PyMethodFlags::FASTCALL.bits()
998-
| rustpython_vm::function::PyMethodFlags::KEYWORDS.bits()
999-
)
1000-
}
1001-
} else if variable_arity {
1002-
quote! { rustpython_vm::function::PyMethodFlags::FASTCALL }
1003-
} else {
1004-
match fixed_positional {
1005-
0 => quote! { rustpython_vm::function::PyMethodFlags::NOARGS },
1006-
1 => quote! { rustpython_vm::function::PyMethodFlags::O },
1007-
_ => quote! { rustpython_vm::function::PyMethodFlags::FASTCALL },
1008-
}
1009-
}
1010-
}

‎crates/vm/src/function/builtin.rs‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{Callee, FromArgs, FuncArgs};
1+
use super::{Callee, FromArgs, FuncArgs, SigArg};
22
use crate::{
33
Py, PyPayload, PyRef, PyResult, VirtualMachine, convert::ToPyResult,
44
object::PyThreadingConstraint,
@@ -37,6 +37,10 @@ impl<F> PyNativeFn for F where
3737
/// just pass an unconstrained generic type, e.g.
3838
/// `fn foo<F, FKind>(f: F) where F: IntoPyNativeFn<FKind>`
3939
pub trait IntoPyNativeFn<Kind>: Sized + PyThreadingConstraint + 'static {
40+
/// Binding metadata of each argument, from which the calling convention
41+
/// is inferred. A `&self` receiver is the `$self` marker.
42+
const ARGS: &'static [SigArg] = &[SigArg::from_arg::<FuncArgs>("args")];
43+
4044
fn call(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult;
4145

4246
/// `IntoPyNativeFn::into_func()` generates a PyNativeFn that performs the
@@ -89,6 +93,8 @@ impl<F, T, R, VM> IntoPyNativeFn<(T, R, VM)> for F
8993
where
9094
F: PyNativeFnInternal<T, R, VM>,
9195
{
96+
const ARGS: &'static [SigArg] = F::ARGS;
97+
9298
#[inline(always)]
9399
fn call(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
94100
self.call_(vm, args, callee)
@@ -98,6 +104,7 @@ where
98104
mod sealed {
99105
use super::*;
100106
pub trait PyNativeFnInternal<T, R, VM>: Sized + PyThreadingConstraint + 'static {
107+
const ARGS: &'static [SigArg];
101108
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult;
102109
}
103110
}
@@ -124,6 +131,8 @@ macro_rules! into_py_native_fn_tuple {
124131
$($T: FromArgs,)*
125132
R: ToPyResult,
126133
{
134+
const ARGS: &'static [SigArg] = &[$(SigArg::from_arg::<$T>("")),*];
135+
127136
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
128137
let ($($n,)*) = args.bind_for::<($($T,)*)>(vm, callee)?;
129138

@@ -138,6 +147,9 @@ macro_rules! into_py_native_fn_tuple {
138147
$($T: FromArgs,)*
139148
R: ToPyResult,
140149
{
150+
const ARGS: &'static [SigArg] =
151+
&[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*];
152+
141153
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
142154
let (zelf, $($n,)*) = args.bind_for::<(PyRef<S>, $($T,)*)>(vm, callee)?;
143155

@@ -152,6 +164,9 @@ macro_rules! into_py_native_fn_tuple {
152164
$($T: FromArgs,)*
153165
R: ToPyResult,
154166
{
167+
const ARGS: &'static [SigArg] =
168+
&[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*];
169+
155170
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
156171
let (zelf, $($n,)*) = args.bind_for::<(PyRef<S>, $($T,)*)>(vm, callee)?;
157172

@@ -165,6 +180,8 @@ macro_rules! into_py_native_fn_tuple {
165180
$($T: FromArgs,)*
166181
R: ToPyResult,
167182
{
183+
const ARGS: &'static [SigArg] = &[$(SigArg::from_arg::<$T>("")),*];
184+
168185
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
169186
let ($($n,)*) = args.bind_for::<($($T,)*)>(vm, callee)?;
170187

@@ -179,6 +196,9 @@ macro_rules! into_py_native_fn_tuple {
179196
$($T: FromArgs,)*
180197
R: ToPyResult,
181198
{
199+
const ARGS: &'static [SigArg] =
200+
&[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*];
201+
182202
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
183203
let (zelf, $($n,)*) = args.bind_for::<(PyRef<S>, $($T,)*)>(vm, callee)?;
184204

@@ -193,6 +213,9 @@ macro_rules! into_py_native_fn_tuple {
193213
$($T: FromArgs,)*
194214
R: ToPyResult,
195215
{
216+
const ARGS: &'static [SigArg] =
217+
&[SigArg::marker("$self") $(, SigArg::from_arg::<$T>(""))*];
218+
196219
fn call_(&self, vm: &VirtualMachine, args: FuncArgs, callee: Callee) -> PyResult {
197220
let (zelf, $($n,)*) = args.bind_for::<(PyRef<S>, $($T,)*)>(vm, callee)?;
198221

‎crates/vm/src/function/method.rs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
descriptor::{PyClassMethodDescriptor, PyMethodDescriptor},
77
},
88
class::PyClassDef,
9-
function::{IntoPyNativeFn, PyNativeFn},
9+
function::{FuncArgs, IntoPyNativeFn, PyNativeFn, SigArg},
1010
};
1111

1212
bitflags::bitflags! {
@@ -49,6 +49,22 @@ bitflags::bitflags! {
4949
impl PyMethodFlags {
5050
// FIXME: macro temp
5151
pub const EMPTY: Self = Self::empty();
52+
53+
const CALL_CONVENTION: Self = Self::VARARGS
54+
.union(Self::KEYWORDS)
55+
.union(Self::NOARGS)
56+
.union(Self::O)
57+
.union(Self::FASTCALL);
58+
59+
/// Adds the calling convention inferred from `args` unless one is set.
60+
/// For METHOD and CLASS the first argument is the bound receiver.
61+
pub(crate) const fn with_call_convention(self, args: &[SigArg]) -> Self {
62+
if self.intersects(Self::CALL_CONVENTION) {
63+
return self;
64+
}
65+
let receiver = self.intersects(Self::METHOD.union(Self::CLASS));
66+
self.union(super::signature::native_call_flags(args, receiver))
67+
}
5268
}
5369

5470
#[macro_export]
@@ -115,16 +131,16 @@ impl PyMethodDef {
115131
}
116132

117133
#[inline]
118-
pub const fn new_const<Kind>(
134+
pub const fn new_const<Kind, F: IntoPyNativeFn<Kind>>(
119135
name: &'static str,
120-
func: impl IntoPyNativeFn<Kind>,
136+
func: F,
121137
flags: PyMethodFlags,
122138
doc: super::ItemDoc,
123139
) -> Self {
124140
Self {
125141
name,
126142
func: super::static_func(func),
127-
flags,
143+
flags: flags.with_call_convention(F::ARGS),
128144
#[cfg(feature = "doc")]
129145
doc_off: doc.offset,
130146
#[cfg(feature = "doc")]
@@ -145,7 +161,7 @@ impl PyMethodDef {
145161
Self {
146162
name,
147163
func: super::static_raw_func(func),
148-
flags,
164+
flags: flags.with_call_convention(&[SigArg::from_arg::<FuncArgs>("args")]),
149165
#[cfg(feature = "doc")]
150166
doc_off: doc.offset,
151167
#[cfg(feature = "doc")]

0 commit comments

Comments
 (0)