Add per-type vectorcall for builtin constructors (#7407)

Add vectorcall fast paths for dict, list, set, int, float, str,
bool, tuple, frozenset. Clear vectorcall when __init__/__new__ is
overridden in Python. Prevent constructor vectorcall inheritance
to heap subclasses. Fix stat_result to use struct_sequence_new
with reference-copy for hidden time fields.
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-03-18 21:44:35 +09:00
1 parent 0768cf80d3
commit 62edc4722a
12 files changed
+233 -26

No files matched your search

+18
View File
@@ -182,8 +182,26 @@ impl Representable for PyBool {
}
}
fn vectorcall_bool(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
pub(crate) fn init(context: &'static Context) {
PyBool::extend_class(context, context.types.bool_type);
context
.types
.bool_type
.slots
.vectorcall
.store(Some(vectorcall_bool));
}
// pub fn not(vm: &VirtualMachine, obj: &PyObject) -> PyResult<bool> {
+23 -1
View File
@@ -15,7 +15,9 @@ use crate::{
class::{PyClassDef, PyClassImpl},
common::ascii,
dict_inner::{self, DictKey},
function::{ArgIterable, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue},
function::{
ArgIterable, FuncArgs, KwArgs, OptionalArg, PyArithmeticValue::*, PyComparisonValue,
},
iter::PyExactSizeIterator,
protocol::{PyIterIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods},
recursion::ReprGuard,
@@ -1433,8 +1435,28 @@ fn set_inner_number_or(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyRes
set_inner_number_op(a, b, |a, b| a.union(b, vm), vm)
}
fn vectorcall_dict(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let obj = PyDict::default().into_ref_with_type(vm, zelf.to_owned())?;
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
PyDict::slot_init(obj.clone().into(), func_args, vm)?;
Ok(obj.into())
}
pub(crate) fn init(context: &'static Context) {
PyDict::extend_class(context, context.types.dict_type);
context
.types
.dict_type
.slots
.vectorcall
.store(Some(vectorcall_dict));
PyDictKeys::extend_class(context, context.types.dict_keys_type);
PyDictKeyIterator::extend_class(context, context.types.dict_keyiterator_type);
PyDictReverseKeyIterator::extend_class(context, context.types.dict_reversekeyiterator_type);
+13
View File
@@ -525,7 +525,20 @@ pub(crate) fn get_value(obj: &PyObject) -> f64 {
obj.downcast_ref::<PyFloat>().unwrap().value
}
fn vectorcall_float(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
#[rustfmt::skip] // to avoid line splitting
pub fn init(context: &'static Context) {
PyFloat::extend_class(context, context.types.float_type);
context.types.float_type.slots.vectorcall.store(Some(vectorcall_float));
}
+18
View File
@@ -791,6 +791,24 @@ pub fn try_to_float(int: &BigInt, vm: &VirtualMachine) -> PyResult<f64> {
.ok_or_else(|| vm.new_overflow_error("int too large to convert to float"))
}
fn vectorcall_int(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
pub(crate) fn init(context: &'static Context) {
PyInt::extend_class(context, context.types.int_type);
context
.types
.int_type
.slots
.vectorcall
.store(Some(vectorcall_int));
}
+15
View File
@@ -768,9 +768,24 @@ impl IterNext for PyListReverseIterator {
}
}
fn vectorcall_list(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let obj = PyList::default().into_ref_with_type(vm, zelf.to_owned())?;
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
PyList::slot_init(obj.clone().into(), func_args, vm)?;
Ok(obj.into())
}
pub fn init(context: &'static Context) {
let list_type = &context.types.list_type;
PyList::extend_class(context, list_type);
list_type.slots.vectorcall.store(Some(vectorcall_list));
PyListIterator::extend_class(context, context.types.list_iterator_type);
PyListReverseIterator::extend_class(context, context.types.list_reverseiterator_type);
+40
View File
@@ -1421,8 +1421,48 @@ impl IterNext for PySetIterator {
}
}
fn vectorcall_set(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let obj = PySet::default().into_ref_with_type(vm, zelf.to_owned())?;
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
PySet::slot_init(obj.clone().into(), func_args, vm)?;
Ok(obj.into())
}
fn vectorcall_frozenset(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
pub fn init(context: &'static Context) {
PySet::extend_class(context, context.types.set_type);
context
.types
.set_type
.slots
.vectorcall
.store(Some(vectorcall_set));
PyFrozenSet::extend_class(context, context.types.frozenset_type);
context
.types
.frozenset_type
.slots
.vectorcall
.store(Some(vectorcall_frozenset));
PySetIterator::extend_class(context, context.types.set_iterator_type);
}
+17
View File
@@ -1781,8 +1781,25 @@ struct ReplaceArgs {
count: isize,
}
fn vectorcall_str(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
pub fn init(ctx: &'static Context) {
PyStr::extend_class(ctx, ctx.types.str_type);
ctx.types
.str_type
.slots
.vectorcall
.store(Some(vectorcall_str));
PyStrIterator::extend_class(ctx, ctx.types.str_iterator_type);
}
+20
View File
@@ -695,9 +695,29 @@ impl IterNext for PyTupleIterator {
}
}
fn vectorcall_tuple(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
nargs: usize,
kwnames: Option<&[PyObjectRef]>,
vm: &VirtualMachine,
) -> PyResult {
let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
// Use the type's own slot_new rather than calling PyTuple::slot_new directly,
// so Rust-level subclasses (e.g. struct sequences) get their custom slot_new called.
(zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
}
pub(crate) fn init(context: &'static Context) {
PyTuple::extend_class(context, context.types.tuple_type);
PyTupleIterator::extend_class(context, context.types.tuple_iterator_type);
context
.types
.tuple_type
.slots
.vectorcall
.store(Some(vectorcall_tuple));
}
pub(super) fn tuple_hash(elements: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyHash> {
+24
View File
@@ -2650,6 +2650,24 @@ fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachin
/// Vectorcall for PyType (PEP 590).
/// Fast path: type(x) returns x.__class__ without constructing FuncArgs.
///
/// # Implementation note: `slots.vectorcall` dual use
///
/// CPython has three distinct fields on PyTypeObject:
/// - `tp_vectorcall`: constructor fast path (e.g. `list_vectorcall`)
/// - `tp_vectorcall_offset`: per-instance vectorcall for callables (e.g. functions)
/// - `tp_call`: standard call slot
///
/// RustPython collapses the first two into a single `slots.vectorcall`. The
/// `call.is_none()` guard below distinguishes the two uses: callable types have
/// `slots.call` set, so their `slots.vectorcall` is for calling instances,
/// not for construction.
///
/// This heuristic is correct for all current builtins but is not a general
/// solution — `type` itself is both callable and has a constructor vectorcall,
/// handled by the explicit `zelf.is(type_type)` check above the guard.
/// If more such types arise, consider splitting into a dedicated
/// `constructor_vectorcall` slot.
fn vectorcall_type(
zelf_obj: &PyObject,
args: Vec<PyObjectRef>,
@@ -2665,6 +2683,12 @@ fn vectorcall_type(
if nargs == 1 && no_kwargs {
return Ok(args[0].obj_type());
}
} else if zelf.slots.call.load().is_none() && zelf.slots.new.load().is_some() {
// Per-type constructor vectorcall for non-callable types (dict, list, int, etc.)
// Also guard on slots.new to avoid dispatching for DISALLOW_INSTANTIATION types.
if let Some(type_vc) = zelf.slots.vectorcall.load() {
return type_vc(zelf_obj, args, nargs, kwnames, vm);
}
}
// Fallback: construct FuncArgs and use standard call
+17 -22
View File
@@ -178,7 +178,6 @@ pub(super) mod _os {
};
use core::time::Duration;
use crossbeam_utils::atomic::AtomicCell;
use itertools::Itertools;
use rustpython_common::wtf8::Wtf8Buf;
use std::{env, fs, fs::OpenOptions, io, path::PathBuf, time::SystemTime};
@@ -1363,29 +1362,25 @@ pub(super) mod _os {
#[pyclass(with(PyStructSequence))]
impl PyStatResult {
#[pyslot]
fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
let flatten_args = |r: &[PyObjectRef]| {
let mut vec_args = Vec::from(r);
loop {
if let Ok(obj) = vec_args.iter().exactly_one() {
match obj.downcast_ref::<PyTuple>() {
Some(t) => {
vec_args = Vec::from(t.as_slice());
}
None => {
return vec_args;
}
}
} else {
return vec_args;
}
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
let seq: PyObjectRef = args.bind(vm)?;
let result = crate::types::struct_sequence_new(cls.clone(), seq, vm)?;
let tuple = result.downcast_ref::<PyTuple>().unwrap();
let mut items: Vec<PyObjectRef> = tuple.to_vec();
// Copy integer time fields to hidden float timestamp slots when not provided.
// indices 7-9: st_atime_int, st_mtime_int, st_ctime_int
// i+3: st_atime/st_mtime/st_ctime (float timestamps, copied from int if missing)
// i+6: st_atime_ns/st_mtime_ns/st_ctime_ns (left as None if not provided)
for i in 7..=9 {
if vm.is_none(&items[i + 3]) {
items[i + 3] = items[i].clone();
}
};
}
let args: FuncArgs = flatten_args(&args.args).into();
let stat: StatResultData = args.bind(vm)?;
Ok(stat.to_pyobject(vm))
PyTuple::new_unchecked(items.into_boxed_slice())
.into_ref_with_type(vm, cls)
.map(Into::into)
}
}
+7 -1
View File
@@ -825,11 +825,17 @@ impl PyType {
}
SlotAccessor::TpIter => update_main_slot!(iter, iter_wrapper, Iter),
SlotAccessor::TpIternext => update_main_slot!(iternext, iternext_wrapper, IterNext),
SlotAccessor::TpInit => update_main_slot!(init, init_wrapper, Init),
SlotAccessor::TpInit => {
update_main_slot!(init, init_wrapper, Init);
if ADD {
self.slots.vectorcall.store(None);
}
}
SlotAccessor::TpNew => {
// __new__ is not wrapped via PyWrapper
if ADD {
self.slots.new.store(Some(new_wrapper));
self.slots.vectorcall.store(None);
} else {
accessor.inherit_from_mro(self);
}
+21 -2
View File
@@ -453,7 +453,20 @@ impl SlotAccessor {
Self::TpStr => inherit_main!(str),
Self::TpCall => {
inherit_main!(call);
inherit_main!(vectorcall);
// CPython does not inherit tp_vectorcall to heap types at all; it only
// inherits tp_vectorcall_offset (the per-instance callable fast path).
// RustPython approximates this by inheriting vectorcall only from types
// with a call slot (instance-call vectorcall), not from types that use
// vectorcall as a constructor fast path (call=None).
// See vectorcall_type() in type.rs for the dual-use design rationale.
let inherited_vc = mro.iter().find_map(|cls| {
if cls.slots.call.load().is_some() {
cls.slots.vectorcall.load()
} else {
None
}
});
typ.slots.vectorcall.store(inherited_vc);
}
Self::TpIter => inherit_main!(iter),
Self::TpIternext => inherit_main!(iternext),
@@ -573,7 +586,13 @@ impl SlotAccessor {
Self::TpStr => copy_main!(str),
Self::TpCall => {
copy_main!(call);
copy_main!(vectorcall);
// See inherit_from_mro TpCall for rationale.
if typ.slots.vectorcall.load().is_none()
&& base.slots.call.load().is_some()
&& let Some(base_val) = base.slots.vectorcall.load()
{
typ.slots.vectorcall.store(Some(base_val));
}
}
Self::TpIter => copy_main!(iter),
Self::TpIternext => copy_main!(iternext),