Tighten specialization guards and add send_none fastpath (#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
This commit is contained in:
Jeong, YunWon
2026-03-06 00:59:53 +09:00
committed by GitHub
parent 9ba155418b
commit 7620c28482
6 changed files with 667 additions and 272 deletions

View File

@@ -1,8 +1,8 @@
use super::Diagnostic;
use crate::util::{
ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta,
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, format_doc, pyclass_ident_and_attrs,
pyexception_ident_and_attrs, text_signature,
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, format_doc, infer_native_call_flags,
pyclass_ident_and_attrs, pyexception_ident_and_attrs, text_signature,
};
use core::str::FromStr;
use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
@@ -1015,6 +1015,16 @@ where
let raw = item_meta.raw()?;
let sig_doc = text_signature(func.sig(), &py_name);
let has_receiver = func
.sig()
.inputs
.iter()
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
let drop_first_typed = match self.inner.attr_name {
AttrName::Method | AttrName::ClassMethod if !has_receiver => 1,
_ => 0,
};
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();
@@ -1031,6 +1041,7 @@ where
doc,
raw,
attr_name: self.inner.attr_name,
call_flags,
});
Ok(())
}
@@ -1248,6 +1259,7 @@ struct MethodNurseryItem {
raw: bool,
doc: Option<String>,
attr_name: AttrName,
call_flags: TokenStream,
}
impl MethodNursery {
@@ -1278,7 +1290,7 @@ impl ToTokens for MethodNursery {
} else {
quote! { None }
};
let flags = match &item.attr_name {
let binding_flags = match &item.attr_name {
AttrName::Method => {
quote! { rustpython_vm::function::PyMethodFlags::METHOD }
}
@@ -1290,6 +1302,12 @@ impl ToTokens for MethodNursery {
}
_ => unreachable!(),
};
let call_flags = &item.call_flags;
let flags = quote! {
rustpython_vm::function::PyMethodFlags::from_bits_retain(
(#binding_flags).bits() | (#call_flags).bits()
)
};
// TODO: intern
// let py_name = if py_name.starts_with("__") && py_name.ends_with("__") {
// let name_ident = Ident::new(&py_name, ident.span());

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, format_doc, iter_use_idents,
pyclass_ident_and_attrs, text_signature,
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, format_doc,
infer_native_call_flags, iter_use_idents, pyclass_ident_and_attrs, text_signature,
};
use core::str::FromStr;
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
@@ -525,6 +525,7 @@ struct FunctionNurseryItem {
cfgs: Vec<Attribute>,
ident: Ident,
doc: String,
call_flags: TokenStream,
}
impl FunctionNursery {
@@ -550,7 +551,6 @@ struct ValidatedFunctionNursery(FunctionNursery);
impl ToTokens for ValidatedFunctionNursery {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut inner_tokens = TokenStream::new();
let flags = quote! { rustpython_vm::function::PyMethodFlags::empty() };
for item in &self.0.items {
let ident = &item.ident;
let cfgs = &item.cfgs;
@@ -558,6 +558,7 @@ impl ToTokens for ValidatedFunctionNursery {
let py_names = &item.py_names;
let doc = &item.doc;
let doc = quote!(Some(#doc));
let flags = &item.call_flags;
inner_tokens.extend(quote![
#(
@@ -706,12 +707,14 @@ impl ModuleItem for FunctionItem {
py_names
}
};
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(),
doc,
call_flags,
});
Ok(())
}

View File

@@ -732,6 +732,77 @@ pub(crate) fn text_signature(sig: &Signature, name: &str) -> String {
}
}
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 syn::FnArg::Typed(typed) = arg else {
continue;
};
let ty_tokens = &typed.ty;
let ty = quote!(#ty_tokens).to_string().replace(' ', "");
// `vm: &VirtualMachine` is not a Python-level argument.
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
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 },
}
}
}
fn func_sig(sig: &Signature) -> String {
sig.inputs
.iter()

View File

@@ -115,6 +115,48 @@ impl Coro {
result
}
fn finalize_send_result(
&self,
jen: &PyObject,
result: PyResult<ExecutionResult>,
vm: &VirtualMachine,
) -> PyResult<PyIterReturn> {
self.maybe_close(&result);
match result {
Ok(exec_res) => Ok(exec_res.into_iter_return(vm)),
Err(e) => {
if e.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
let err =
vm.new_runtime_error(format!("{} raised StopIteration", gen_name(jen, vm)));
err.set___cause__(Some(e));
Err(err)
} else if jen.class().is(vm.ctx.types.async_generator)
&& e.fast_isinstance(vm.ctx.exceptions.stop_async_iteration)
{
let err = vm.new_runtime_error("async generator raised StopAsyncIteration");
err.set___cause__(Some(e));
Err(err)
} else {
Err(e)
}
}
}
}
pub(crate) fn send_none(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
if self.closed.load() {
return Ok(PyIterReturn::StopIteration(None));
}
self.frame.locals_to_fast(vm)?;
let value = if self.frame.lasti() > 0 {
Some(vm.ctx.none())
} else {
None
};
let result = self.run_with_context(jen, vm, |f| f.resume(value, vm));
self.finalize_send_result(jen, result, vm)
}
pub fn send(
&self,
jen: &PyObject,
@@ -136,26 +178,7 @@ impl Coro {
None
};
let result = self.run_with_context(jen, vm, |f| f.resume(value, vm));
self.maybe_close(&result);
match result {
Ok(exec_res) => Ok(exec_res.into_iter_return(vm)),
Err(e) => {
if e.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
let err =
vm.new_runtime_error(format!("{} raised StopIteration", gen_name(jen, vm)));
err.set___cause__(Some(e));
Err(err)
} else if jen.class().is(vm.ctx.types.async_generator)
&& e.fast_isinstance(vm.ctx.exceptions.stop_async_iteration)
{
let err = vm.new_runtime_error("async generator raised StopAsyncIteration");
err.set___cause__(Some(e));
Err(err)
} else {
Err(e)
}
}
}
self.finalize_send_result(jen, result, vm)
}
pub fn throw(

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,11 @@ bitflags::bitflags! {
// METH_XXX flags in CPython
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PyMethodFlags: u32 {
// const VARARGS = 0x0001;
// const KEYWORDS = 0x0002;
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;
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
@@ -31,7 +31,7 @@ bitflags::bitflags! {
// const COEXIST = 0x0040;
// if not Py_LIMITED_API
// const FASTCALL = 0x0080;
const FASTCALL = 0x0080;
// This bit is preserved for Stackless Python
// const STACKLESS = 0x0100;