Skip to content

Commit 7620c28

Browse files
authored
Tighten specialization guards and add send_none fastpath (RustPython#7359)
* vm: align specialization guards with CPython patterns * vm: tighten call specialization runtime guards * vm: add send_none fastpath for generator specialization * vm: restrict method-descriptor specialization to methods * vm: deopt call specializations on guard misses * vm: match CPython send/for-iter closed-frame guards * vm: restrict len/isinstance specialization to builtins * vm: use exact-type guards for call specializations * vm: align class-call specialization flow with CPython * vm: prefer FAST call opcodes for positional builtin calls * vm: add callable identity guard to CALL_LIST_APPEND * vm: make CALL_LIST_APPEND runtime guard pointer-based * vm: align call guard cache and fallback behavior with CPython * vm: use base vectorcall fallback for EXIT-style call misses * vm: simplify CALL_LEN/CALL_ISINSTANCE runtime guards * vm: infer call-convention flags for CPython-style CALL specialization * vm: check use_tracing in eval_frame_active, add SendGen send_none - Implement specialization_eval_frame_active to check vm.use_tracing so specializations are skipped when tracing/profiling is active - Add send_none fastpath in SendGen handler for the common None case
1 parent 9ba1554 commit 7620c28

6 files changed

Lines changed: 663 additions & 268 deletions

File tree

crates/derive-impl/src/pyclass.rs

Lines changed: 21 additions & 3 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, format_doc, pyclass_ident_and_attrs,
5-
pyexception_ident_and_attrs, text_signature,
4+
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, format_doc, infer_native_call_flags,
5+
pyclass_ident_and_attrs, pyexception_ident_and_attrs, text_signature,
66
};
77
use core::str::FromStr;
88
use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
@@ -1015,6 +1015,16 @@ where
10151015

10161016
let raw = item_meta.raw()?;
10171017
let sig_doc = text_signature(func.sig(), &py_name);
1018+
let has_receiver = func
1019+
.sig()
1020+
.inputs
1021+
.iter()
1022+
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
1023+
let drop_first_typed = match self.inner.attr_name {
1024+
AttrName::Method | AttrName::ClassMethod if !has_receiver => 1,
1025+
_ => 0,
1026+
};
1027+
let call_flags = infer_native_call_flags(func.sig(), drop_first_typed);
10181028

10191029
// Add #[allow(non_snake_case)] for setter methods like set___name__
10201030
let method_name = ident.to_string();
@@ -1031,6 +1041,7 @@ where
10311041
doc,
10321042
raw,
10331043
attr_name: self.inner.attr_name,
1044+
call_flags,
10341045
});
10351046
Ok(())
10361047
}
@@ -1248,6 +1259,7 @@ struct MethodNurseryItem {
12481259
raw: bool,
12491260
doc: Option<String>,
12501261
attr_name: AttrName,
1262+
call_flags: TokenStream,
12511263
}
12521264

12531265
impl MethodNursery {
@@ -1278,7 +1290,7 @@ impl ToTokens for MethodNursery {
12781290
} else {
12791291
quote! { None }
12801292
};
1281-
let flags = match &item.attr_name {
1293+
let binding_flags = match &item.attr_name {
12821294
AttrName::Method => {
12831295
quote! { rustpython_vm::function::PyMethodFlags::METHOD }
12841296
}
@@ -1290,6 +1302,12 @@ impl ToTokens for MethodNursery {
12901302
}
12911303
_ => unreachable!(),
12921304
};
1305+
let call_flags = &item.call_flags;
1306+
let flags = quote! {
1307+
rustpython_vm::function::PyMethodFlags::from_bits_retain(
1308+
(#binding_flags).bits() | (#call_flags).bits()
1309+
)
1310+
};
12931311
// TODO: intern
12941312
// let py_name = if py_name.starts_with("__") && py_name.ends_with("__") {
12951313
// let name_ident = Ident::new(&py_name, ident.span());

crates/derive-impl/src/pymodule.rs

Lines changed: 6 additions & 3 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, format_doc, iter_use_idents,
6-
pyclass_ident_and_attrs, text_signature,
5+
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, format_doc,
6+
infer_native_call_flags, iter_use_idents, pyclass_ident_and_attrs, text_signature,
77
};
88
use core::str::FromStr;
99
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
@@ -525,6 +525,7 @@ struct FunctionNurseryItem {
525525
cfgs: Vec<Attribute>,
526526
ident: Ident,
527527
doc: String,
528+
call_flags: TokenStream,
528529
}
529530

530531
impl FunctionNursery {
@@ -550,14 +551,14 @@ struct ValidatedFunctionNursery(FunctionNursery);
550551
impl ToTokens for ValidatedFunctionNursery {
551552
fn to_tokens(&self, tokens: &mut TokenStream) {
552553
let mut inner_tokens = TokenStream::new();
553-
let flags = quote! { rustpython_vm::function::PyMethodFlags::empty() };
554554
for item in &self.0.items {
555555
let ident = &item.ident;
556556
let cfgs = &item.cfgs;
557557
let cfgs = quote!(#(#cfgs)*);
558558
let py_names = &item.py_names;
559559
let doc = &item.doc;
560560
let doc = quote!(Some(#doc));
561+
let flags = &item.call_flags;
561562

562563
inner_tokens.extend(quote![
563564
#(
@@ -706,12 +707,14 @@ impl ModuleItem for FunctionItem {
706707
py_names
707708
}
708709
};
710+
let call_flags = infer_native_call_flags(func.sig(), 0);
709711

710712
args.context.function_items.add_item(FunctionNurseryItem {
711713
ident: ident.to_owned(),
712714
py_names,
713715
cfgs: args.cfgs.to_vec(),
714716
doc,
717+
call_flags,
715718
});
716719
Ok(())
717720
}

crates/derive-impl/src/util.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,77 @@ pub(crate) fn text_signature(sig: &Signature, name: &str) -> String {
732732
}
733733
}
734734

735+
pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize) -> TokenStream {
736+
// Best-effort mapping of Rust function signatures to CPython-style
737+
// METH_* calling convention flags used by CALL specialization.
738+
let mut typed_args = Vec::new();
739+
for arg in &sig.inputs {
740+
let syn::FnArg::Typed(typed) = arg else {
741+
continue;
742+
};
743+
let ty_tokens = &typed.ty;
744+
let ty = quote!(#ty_tokens).to_string().replace(' ', "");
745+
// `vm: &VirtualMachine` is not a Python-level argument.
746+
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
747+
continue;
748+
}
749+
typed_args.push(ty);
750+
}
751+
752+
let mut user_args = typed_args.into_iter();
753+
for _ in 0..drop_first_typed {
754+
if user_args.next().is_none() {
755+
break;
756+
}
757+
}
758+
759+
let mut has_keywords = false;
760+
let mut variable_arity = false;
761+
let mut fixed_positional = 0usize;
762+
763+
for ty in user_args {
764+
let is_named = |name: &str| {
765+
ty == name
766+
|| ty.starts_with(&format!("{name}<"))
767+
|| ty.contains(&format!("::{name}<"))
768+
|| ty.ends_with(&format!("::{name}"))
769+
};
770+
771+
if is_named("FuncArgs") {
772+
has_keywords = true;
773+
variable_arity = true;
774+
continue;
775+
}
776+
if is_named("KwArgs") {
777+
has_keywords = true;
778+
variable_arity = true;
779+
continue;
780+
}
781+
if is_named("PosArgs") || is_named("OptionalArg") || is_named("OptionalOption") {
782+
variable_arity = true;
783+
continue;
784+
}
785+
fixed_positional += 1;
786+
}
787+
788+
if has_keywords {
789+
quote! {
790+
rustpython_vm::function::PyMethodFlags::from_bits_retain(
791+
rustpython_vm::function::PyMethodFlags::FASTCALL.bits()
792+
| rustpython_vm::function::PyMethodFlags::KEYWORDS.bits()
793+
)
794+
}
795+
} else if variable_arity {
796+
quote! { rustpython_vm::function::PyMethodFlags::FASTCALL }
797+
} else {
798+
match fixed_positional {
799+
0 => quote! { rustpython_vm::function::PyMethodFlags::NOARGS },
800+
1 => quote! { rustpython_vm::function::PyMethodFlags::O },
801+
_ => quote! { rustpython_vm::function::PyMethodFlags::FASTCALL },
802+
}
803+
}
804+
}
805+
735806
fn func_sig(sig: &Signature) -> String {
736807
sig.inputs
737808
.iter()

crates/vm/src/coroutine.rs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,12 @@ impl Coro {
115115
result
116116
}
117117

118-
pub fn send(
118+
fn finalize_send_result(
119119
&self,
120120
jen: &PyObject,
121-
value: PyObjectRef,
121+
result: PyResult<ExecutionResult>,
122122
vm: &VirtualMachine,
123123
) -> PyResult<PyIterReturn> {
124-
if self.closed.load() {
125-
return Ok(PyIterReturn::StopIteration(None));
126-
}
127-
self.frame.locals_to_fast(vm)?;
128-
let value = if self.frame.lasti() > 0 {
129-
Some(value)
130-
} else if !vm.is_none(&value) {
131-
return Err(vm.new_type_error(format!(
132-
"can't send non-None value to a just-started {}",
133-
gen_name(jen, vm),
134-
)));
135-
} else {
136-
None
137-
};
138-
let result = self.run_with_context(jen, vm, |f| f.resume(value, vm));
139124
self.maybe_close(&result);
140125
match result {
141126
Ok(exec_res) => Ok(exec_res.into_iter_return(vm)),
@@ -158,6 +143,44 @@ impl Coro {
158143
}
159144
}
160145

146+
pub(crate) fn send_none(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
147+
if self.closed.load() {
148+
return Ok(PyIterReturn::StopIteration(None));
149+
}
150+
self.frame.locals_to_fast(vm)?;
151+
let value = if self.frame.lasti() > 0 {
152+
Some(vm.ctx.none())
153+
} else {
154+
None
155+
};
156+
let result = self.run_with_context(jen, vm, |f| f.resume(value, vm));
157+
self.finalize_send_result(jen, result, vm)
158+
}
159+
160+
pub fn send(
161+
&self,
162+
jen: &PyObject,
163+
value: PyObjectRef,
164+
vm: &VirtualMachine,
165+
) -> PyResult<PyIterReturn> {
166+
if self.closed.load() {
167+
return Ok(PyIterReturn::StopIteration(None));
168+
}
169+
self.frame.locals_to_fast(vm)?;
170+
let value = if self.frame.lasti() > 0 {
171+
Some(value)
172+
} else if !vm.is_none(&value) {
173+
return Err(vm.new_type_error(format!(
174+
"can't send non-None value to a just-started {}",
175+
gen_name(jen, vm),
176+
)));
177+
} else {
178+
None
179+
};
180+
let result = self.run_with_context(jen, vm, |f| f.resume(value, vm));
181+
self.finalize_send_result(jen, result, vm)
182+
}
183+
161184
pub fn throw(
162185
&self,
163186
jen: &PyObject,

0 commit comments

Comments
 (0)